@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 +11 -0
- package/cli/lib/assistant.mjs +17 -7
- package/cli/lib/config.mjs +8 -1
- package/cli/lib/i18n.mjs +6 -0
- package/cli/lib/pet-server.mjs +104 -14
- package/cli/pet/electron/main.cjs +205 -41
- package/cli/pet/pet.html +94 -10
- package/cli/pet/settings.html +430 -0
- package/package.json +1 -1
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':
|
package/cli/lib/assistant.mjs
CHANGED
|
@@ -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);
|
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
|
@@ -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}',
|
package/cli/lib/pet-server.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
79
|
-
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);
|
|
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
|
-
|
|
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
|
|
131
|
-
fail: (e) => `In the terminal, the command \`${e.cmd
|
|
132
|
-
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'}.`,
|
|
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
|
-
|
|
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',
|
|
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();
|