@7ots/cli 0.1.3 → 0.1.5

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/cli/7ots.mjs CHANGED
@@ -232,6 +232,14 @@ async function cmdReminders({ flags }) {
232
232
  if (r.notes.length) console.log(`\n${bold('notes')}\n${r.notes.map((n) => ` · ${n.text}`).join('\n')}`);
233
233
  }
234
234
 
235
+ /** The settings page (brain, what it may see, notes, reminders), in the browser. --print: only the URL. */
236
+ async function cmdConfig({ flags }) {
237
+ if (!(await petAlive())) fail(t('err.noPet'));
238
+ const url = `http://127.0.0.1:${PET_PORT}/settings?token=${petToken()}`;
239
+ if (flags.print || !graphical() || !openUrl(url)) return console.log(url);
240
+ console.log(t('pet.settings'));
241
+ }
242
+
235
243
  async function cmdPet({ flags }) {
236
244
  if (flags.daemon) {
237
245
  try {
@@ -457,6 +465,9 @@ async function main() {
457
465
  return cmdAsk(args);
458
466
  case 'reminders':
459
467
  return cmdReminders(args);
468
+ case 'config':
469
+ case 'settings':
470
+ return cmdConfig(args);
460
471
  case 'status':
461
472
  return cmdStatus(args);
462
473
  case 'stop':
@@ -235,9 +235,11 @@ function notify(title, body) {
235
235
  }
236
236
 
237
237
  /**
238
- * @param {{ brain, system: () => string, lang: string, name: string, onFire: (reminder, line) => void, log?: Function }} o
238
+ * @param {{ brain, system: () => string, lang: string, name: string, onFire: (reminder, line) => void, access?: () => object, log?: Function }} o
239
+ * access() config.access: memory (keep/use notes), open (open sites), notify (desktop notifications)
239
240
  */
240
- export function createAssistant({ brain, system, lang, name, onFire, log = () => {} }) {
241
+ export function createAssistant({ brain, system, lang, name, onFire, access = () => ({}), log = () => {} }) {
242
+ const may = (k) => access()[k] !== false;
241
243
  let L = tr(lang);
242
244
  const remF = homeFile('reminders.json');
243
245
  const memF = homeFile('memory.json');
@@ -249,8 +251,8 @@ export function createAssistant({ brain, system, lang, name, onFire, log = () =>
249
251
  function fire(r) {
250
252
  r.done = true;
251
253
  const line = tr(r.lang || lang).fire(r);
252
- notify(`${name} · 7ots`, r.text);
253
- if (r.open) openUrl(r.open);
254
+ if (may('notify')) notify(`${name} · 7ots`, r.text);
255
+ if (r.open && may('open')) openUrl(r.open);
254
256
  onFire(r, line);
255
257
  }
256
258
 
@@ -269,8 +271,9 @@ export function createAssistant({ brain, system, lang, name, onFire, log = () =>
269
271
  function run(actions) {
270
272
  const out = [];
271
273
  for (const a of actions) {
274
+ if ((a.type === 'note' && !may('memory')) || (a.type === 'open' && !may('open'))) continue;
272
275
  if (a.type === 'remind') {
273
- const r = { id: randomBytes(4).toString('hex'), at: a.at, text: a.text, open: a.open || null, lang: L === T.es ? 'es' : L === T.pt ? 'pt' : 'en', created: Date.now() };
276
+ const r = { id: randomBytes(4).toString('hex'), at: a.at, text: a.text, open: (may('open') && a.open) || null, lang: L === T.es ? 'es' : L === T.pt ? 'pt' : 'en', created: Date.now() };
274
277
  reminders.push(r);
275
278
  reminders.sort((x, y) => x.at - y.at);
276
279
  saveR();
@@ -288,7 +291,7 @@ export function createAssistant({ brain, system, lang, name, onFire, log = () =>
288
291
  saveR();
289
292
  out.push(L.cancelled(n));
290
293
  } else if (a.type === 'list') out.push(reminders.length ? L.list(reminders) : L.none);
291
- else if (a.type === 'know') out.push(L.know(notes.slice(-12)));
294
+ else if (a.type === 'know') out.push(L.know(may('memory') ? notes.slice(-12) : []));
292
295
  else if (a.type === 'ask_when') out.push(L.when);
293
296
  else if (a.type === 'help') out.push(L.help);
294
297
  }
@@ -311,7 +314,7 @@ export function createAssistant({ brain, system, lang, name, onFire, log = () =>
311
314
  let ai = null;
312
315
  if (brain.kind !== 'lines') {
313
316
  try {
314
- const raw = await brain.think({ system: system(), messages: [{ role: 'user', content: aiPrompt(text, { lang: said, notes: notes.slice(-30), reminders, now }) }], timeoutMs: 45000, raw: true });
317
+ const raw = await brain.think({ system: system(), messages: [{ role: 'user', content: aiPrompt(text, { lang: said, notes: may('memory') ? notes.slice(-30) : [], reminders, now }) }], timeoutMs: 45000, raw: true });
315
318
  ai = parseAi(raw, now);
316
319
  } catch (e) {
317
320
  log(`assistant: ${e.message}`);
@@ -324,6 +327,13 @@ export function createAssistant({ brain, system, lang, name, onFire, log = () =>
324
327
  const done = run(local);
325
328
  return { reply: done.join(' ') || L.help, actions: local };
326
329
  },
330
+ /** Forget one note (by its timestamp, stable across the list) or all of them. */
331
+ forget(at) {
332
+ const n = notes.length;
333
+ notes = at === 'all' ? [] : notes.filter((x) => String(x.at) !== String(at));
334
+ saveN();
335
+ return n !== notes.length;
336
+ },
327
337
  cancel(id) {
328
338
  const n = reminders.length;
329
339
  reminders = reminders.filter((r) => r.id !== id);
@@ -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
@@ -18,6 +18,7 @@ const M = {
18
18
  7ots status [--line|--json] pet state (--line: one line for tmux)
19
19
  7ots ask <text> talk to it: "remind me to check email in 10 min", "open my calendar", "remember that…"
20
20
  7ots reminders [--cancel <id>] pending reminders and what it remembers
21
+ 7ots config [--print] settings page: brain, what it can see, notes
21
22
  7ots hooks install|remove [--claude] [--shell] [--global]
22
23
  7ots meet --new | "<invite or call URL>" join apuchat meet with this identity
23
24
  7ots prompt [identity|install|pet|meet|all] prompts for coding agents
@@ -82,6 +83,7 @@ Docs: https://7ots.com/llms.txt`,
82
83
  'login.ok': 'Connected to Orquesta ({org}).',
83
84
  'err.unknown': 'Unknown command "{cmd}". Try: 7ots help',
84
85
  'err.noPet': 'The pet is not awake. Start it with: 7ots pet',
86
+ 'pet.settings': 'Opening the settings in your browser.',
85
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.',
86
88
  'err.noIdentity': 'No identity yet. Create one with "7ots new" or run "7ots setup".',
87
89
  'w.title': '7ots setup — {name}',
@@ -187,6 +189,7 @@ Docs: https://7ots.com/llms.txt`,
187
189
  7ots status [--line|--json] estado (--line: una línea para tmux)
188
190
  7ots ask <texto> háblale: "recuérdame ver el correo en 10 min", "abre mi calendario", "anota que…"
189
191
  7ots reminders [--cancel <id>] recordatorios pendientes y lo que recuerda
192
+ 7ots config [--print] ajustes: cerebro, qué puede ver, notas
190
193
  7ots hooks install|remove [--claude] [--shell] [--global]
191
194
  7ots meet --new | "<invitación o enlace>" entra a apuchat meet con esta identidad
192
195
  7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
@@ -251,6 +254,7 @@ Docs: https://7ots.com/llms.txt`,
251
254
  'login.ok': 'Conectado a Orquesta ({org}).',
252
255
  'err.unknown': 'Comando desconocido «{cmd}». Prueba: 7ots help',
253
256
  'err.noPet': 'La mascota no está despierta. Iníciala con: 7ots pet',
257
+ 'pet.settings': 'Abriendo los ajustes en tu navegador.',
254
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.',
255
259
  'err.noIdentity': 'Aún no hay identidad. Crea una con «7ots new» o corre «7ots setup».',
256
260
  'w.title': 'Configuración de 7ots — {name}',
@@ -356,6 +360,7 @@ Docs: https://7ots.com/llms.txt`,
356
360
  7ots status [--line|--json] estado (--line: uma linha para tmux)
357
361
  7ots ask <texto> fale com ele: "me lembre de ver o e-mail em 10 min", "abra minha agenda", "anote que…"
358
362
  7ots reminders [--cancel <id>] lembretes pendentes e o que ele lembra
363
+ 7ots config [--print] ajustes: cérebro, o que pode ver, notas
359
364
  7ots hooks install|remove [--claude] [--shell] [--global]
360
365
  7ots meet --new | "<convite ou link>" entra no apuchat meet com esta identidade
361
366
  7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
@@ -420,6 +425,7 @@ Docs: https://7ots.com/llms.txt`,
420
425
  'login.ok': 'Conectado ao Orquesta ({org}).',
421
426
  'err.unknown': 'Comando desconhecido «{cmd}». Tente: 7ots help',
422
427
  'err.noPet': 'O bichinho não está acordado. Inicie com: 7ots pet',
428
+ 'pet.settings': 'Abrindo os ajustes no seu navegador.',
423
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.',
424
430
  'err.noIdentity': 'Ainda não há identidade. Crie uma com «7ots new» ou rode «7ots setup».',
425
431
  'w.title': 'Configuração do 7ots — {name}',
@@ -11,6 +11,11 @@
11
11
  * POST /ask X-7ots-Token { text } → { reply, actions } reminders, notes, open a site (assistant.mjs)
12
12
  * GET /reminders?token=… { reminders, notes }
13
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' }
14
19
  * POST /quit X-7ots-Token
15
20
  *
16
21
  * Only listens on 127.0.0.1 and only answers to Host 127.0.0.1/localhost (no DNS rebinding).
@@ -22,8 +27,8 @@ import { randomBytes, timingSafeEqual } from 'node:crypto';
22
27
  import { readFileSync, existsSync, writeFileSync, chmodSync } from 'node:fs';
23
28
  import { join } from 'node:path';
24
29
  import { createAssistant } from './assistant.mjs';
25
- import { createBrain, petSystem } from './brain.mjs';
26
- import { loadConfig, env } from './config.mjs';
30
+ import { AI_CLIS, createBrain, detectClis, petSystem } from './brain.mjs';
31
+ import { env, loadConfig, saveConfig, saveKey } from './config.mjs';
27
32
  import { loadIdentity } from './identity.mjs';
28
33
  import { line } from './lines.mjs';
29
34
  import { ensureHome, homeFile, PET_PORT, PKG_ROOT } from './paths.mjs';
@@ -67,6 +72,7 @@ export async function postPet(path, body, port = PET_PORT) {
67
72
  }
68
73
  }
69
74
 
75
+ const SETTABLE_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ORQUESTA_TOKEN'];
70
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');
71
77
 
72
78
  const GAP = { 0: Infinity, 1: 10 * 60_000, 2: 90_000, 3: 20_000 }; // minimum silence between spontaneous lines
@@ -75,12 +81,13 @@ const IDLE = { 0: Infinity, 1: 60 * 60_000, 2: 20 * 60_000, 3: 5 * 60_000 }; //
75
81
 
76
82
  export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
77
83
  const token = petToken();
78
- const config = loadConfig();
79
- const annoy = Math.max(0, Math.min(3, Number(config.pet.annoy ?? 2)));
84
+ let config = loadConfig();
85
+ const annoyOf = (c) => Math.max(0, Math.min(3, Number(c.pet.annoy ?? 2)));
86
+ let annoy = annoyOf(config);
80
87
  const { identity } = loadIdentity();
81
88
  if (!identity) throw Object.assign(new Error('no identity'), { code: 'NO_IDENTITY' });
82
89
  const lang = identity.language || 'en';
83
- const brain = createBrain(config.brain, config.home);
90
+ let brain = createBrain(config.brain, config.home); // swapped live from the settings page
84
91
  const pet = tick(loadPet());
85
92
  const clients = new Set();
86
93
  const history = []; // last lines (so the AI doesn't repeat itself)
@@ -125,11 +132,11 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
125
132
  }
126
133
 
127
134
  const SITUATION = {
128
- tool: (e) => `Your human's coding agent just used the tool ${e.tool}${e.detail ? ` (${e.detail})` : ''}.`,
129
- toolFail: (e) => `Your human's coding agent tried the tool ${e.tool} and it FAILED${e.detail ? `: ${e.detail}` : ''}.`,
130
- ok: (e) => `In the terminal, the command \`${e.cmd || '?'}\` just succeeded.`,
131
- fail: (e) => `In the terminal, the command \`${e.cmd || '?'}\` just failed with exit code ${e.code}.`,
132
- prompt: (e) => `Your human just asked their coding agent: "${String(e.text || '').slice(0, 300)}".`,
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'}.`,
133
140
  stop: () => `Your human's coding agent just finished its turn. Encourage them to check the result.`,
134
141
  waiting: (e) => `Your human's coding agent is waiting for them (permission or an answer)${e.message ? `: ${e.message}` : ''}. Get their attention.`,
135
142
  hello: () => `You just woke up on your human's desktop. Say hi.`,
@@ -175,7 +182,9 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
175
182
  if (!kind) return broadcast({ gesture: r.gesture });
176
183
  const routine = !r.important && !['fail', 'toolFail'].includes(kind);
177
184
  if (routine && Math.random() >= CHATTY[annoy]) return broadcast({ gesture: r.gesture });
178
- const sit = SITUATION[kind]?.(ev) || '';
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) || '';
179
188
  speak(kind, { cmd: ev.cmd || '', tool: ev.tool || '' }, sit, { force: r.important && ev.type !== 'tool', gesture: r.gesture });
180
189
  }
181
190
 
@@ -195,7 +204,8 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
195
204
 
196
205
  // The assistant: reminders that speak, notify and open the tab; notes it learns.
197
206
  const assistant = createAssistant({
198
- brain,
207
+ brain: { get kind() { return brain.kind; }, think: (o) => brain.think(o) },
208
+ access: () => config.access,
199
209
  lang,
200
210
  name: identity.name,
201
211
  log,
@@ -230,8 +240,8 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
230
240
  return a.length === b.length && timingSafeEqual(a, b);
231
241
  };
232
242
 
233
- const page = () =>
234
- readFileSync(join(PKG_ROOT, 'cli', 'pet', 'pet.html'), 'utf8')
243
+ const page = (file = 'pet.html') =>
244
+ readFileSync(join(PKG_ROOT, 'cli', 'pet', file), 'utf8')
235
245
  .replace('__BOOT__', JSON.stringify({ token, identity, voice: config.voice, lang, annoy, brain: brainLabel(config.brain), activity }).replace(/</g, '\\u003c'));
236
246
 
237
247
  const server = createServer(async (req, res) => {
@@ -253,6 +263,10 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
253
263
  if (req.method === 'GET' && p === '/pet') {
254
264
  return res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }).end(page());
255
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());
256
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 } });
257
271
  if (req.method === 'GET' && p === '/events') {
258
272
  res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
@@ -297,6 +311,23 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
297
311
  broadcast({ thinking: false, say: r.reply, gesture: r.actions.length ? 'nod' : null });
298
312
  return json(200, r);
299
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 ?? '')) });
300
331
  if (p === '/reminders/cancel') return json(200, { ok: assistant.cancel(String(data.id || '')) });
301
332
  if (p === '/quit') {
302
333
  json(200, { ok: true });
@@ -305,6 +336,65 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
305
336
  return json(404, { error: 'not found' });
306
337
  });
307
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
+
308
398
  function stop() {
309
399
  clearInterval(timer);
310
400
  assistant.stop();