@7ots/cli 0.1.6 → 0.1.8
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/README.md +3 -3
- package/cli/7ots.mjs +13 -1
- package/cli/lib/account.mjs +152 -2
- package/cli/lib/assistant.mjs +107 -38
- package/cli/lib/brain.mjs +13 -2
- package/cli/lib/config.mjs +5 -1
- package/cli/lib/inferences.mjs +30 -6
- package/cli/lib/integrations.mjs +69 -5
- package/cli/lib/orquesta.mjs +44 -1
- package/cli/lib/pet-server.mjs +352 -11
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/pet/electron/director.cjs +37 -5
- package/cli/pet/electron/main.cjs +92 -10
- package/cli/pet/pet.html +149 -10
- package/cli/pet/settings.html +549 -43
- package/dist/7ots.esm.js +216 -137
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +216 -137
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +1 -1
- package/package.json +17 -14
- package/server/channels/agent.mjs +7 -1
- package/server/channels/index.mjs +3 -2
- package/server/platform/connect.mjs +4 -1
- package/server/platform/db.mjs +14 -1
- package/server/platform/device.mjs +101 -4
- package/server/platform/mods.mjs +222 -0
- package/server/platform/orquesta.mjs +78 -14
- package/server/platform/profile.mjs +161 -0
- package/server/platform/routes.mjs +39 -33
- package/server/platform/runtime.mjs +5 -3
- package/server/platform/store.mjs +1 -1
- package/server/platform/vm.mjs +688 -17
- package/server/server.mjs +12 -2
- package/server/tts.mjs +17 -4
- package/src/avatar/three.js +3 -2
- package/src/character/AvatarEditor.js +95 -7
- package/src/character/Character.js +116 -17
- package/src/character/TradingCard.js +226 -0
- package/src/character/mods/index.js +4 -0
- package/src/character/mods/orc.js +64 -0
- package/src/character/mods.js +251 -0
- package/src/character/more/acc-head.js +213 -0
- package/src/character/more/acc-rest.js +342 -0
- package/src/character/more/brows.js +39 -0
- package/src/character/more/cheeks.js +78 -0
- package/src/character/more/eyes.js +288 -0
- package/src/character/more/mouths.js +259 -0
- package/src/character/more/patterns.js +120 -0
- package/src/character/parts.js +100 -0
- package/src/character/skin.js +166 -1
- package/src/character/styles.js +1285 -9
- package/src/character3d/Character3D.js +253 -37
- package/src/character3d/index.js +7 -1
- package/src/character3d/model.js +367 -0
- package/src/i18n/messages/character.js +37 -22
- package/src/i18n/messages/platform.js +33 -0
- package/src/identity/random.js +17 -3
- package/src/identity/schema.js +10 -0
- package/src/index.js +1 -0
package/cli/lib/pet-server.mjs
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* POST /config { brain?, pet?, access?, keys? } → saved to ~/.7ots and applied live
|
|
27
27
|
* POST /config/test one line from the current brain → { ok, text, ms }
|
|
28
28
|
* POST /notes/forget { at | 'all' }
|
|
29
|
+
* POST /conversation/forget drop the conversation memory (turns + summary)
|
|
29
30
|
* POST /quit X-7ots-Token
|
|
30
31
|
*
|
|
31
32
|
* Only listens on 127.0.0.1 and only answers to Host 127.0.0.1/localhost (no DNS rebinding).
|
|
@@ -33,27 +34,39 @@
|
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
36
|
import { createServer } from 'node:http';
|
|
36
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
37
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
37
38
|
import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, symlinkSync } from 'node:fs';
|
|
38
39
|
import { cardShot } from './card-shot.mjs';
|
|
39
40
|
import { join } from 'node:path';
|
|
40
41
|
import { createAssistant, notify, tr } from './assistant.mjs';
|
|
41
42
|
import { createWatchers, processes, processSummary } from './watchers.mjs';
|
|
42
|
-
import { createInferenceLog } from './inferences.mjs';
|
|
43
|
+
import { createInferenceLog, otherInferences } from './inferences.mjs';
|
|
44
|
+
|
|
45
|
+
const MAX_INF = 300;
|
|
43
46
|
import { AI_CLIS, createBrain, detectClis, petSystem } from './brain.mjs';
|
|
44
47
|
import { env, loadConfig, saveConfig, saveKey } from './config.mjs';
|
|
45
48
|
import { loadIdentity } from './identity.mjs';
|
|
49
|
+
import { readJson, writeJson } from './paths.mjs';
|
|
46
50
|
import { sttProvider, transcribe } from './stt.mjs';
|
|
47
51
|
import { localVoices, synthesizeLocal, warmLocal } from './tts-local.mjs';
|
|
48
52
|
import { createScreenWatcher } from './screen.mjs';
|
|
49
|
-
import {
|
|
53
|
+
import { createSiteContext } from './prowl.mjs';
|
|
54
|
+
import { SEVENOTS_URL, community, sync as syncOt, logout as accountLogout, me as accountMe, pull as pullOt, signedIn, startLogin, use as useOt } from './account.mjs';
|
|
50
55
|
import { openUrl } from './open.mjs';
|
|
51
56
|
import { VM_KINDS } from './vm.mjs';
|
|
52
57
|
import { cloudStatus, createExtras, lettaSync, normalizeCharacter, normalizeIntegrations } from './integrations.mjs';
|
|
53
58
|
import { spawn } from 'node:child_process';
|
|
54
59
|
import { openSync } from 'node:fs';
|
|
55
60
|
import { line } from './lines.mjs';
|
|
56
|
-
import { defineIdentity } from '../../src/identity/schema.js';
|
|
61
|
+
import { defineIdentity, GROK_VOICES, OPENAI_VOICES } from '../../src/identity/schema.js';
|
|
62
|
+
import { normalizeMod, normalizeMods, MAX_MODS } from '../../src/character/mods.js';
|
|
63
|
+
import { MOD_CATALOG } from '../../src/character/mods/index.js';
|
|
64
|
+
|
|
65
|
+
/** Mods importados en este pet (~/.7ots/mods/library.json), normalizados. */
|
|
66
|
+
// scenes between ots on the same desktop (pet/electron/director.cjs plays them)
|
|
67
|
+
const SCENE_NAMES = ['rocky', 'sf', 'dbz', 'matrix', 'duel', 'sw', 'fight', 'twist'];
|
|
68
|
+
const SCENE_EVERY = ['rare', 'normal', 'often'];
|
|
69
|
+
const modLibrary = () => (readJson(homeFile('mods/library.json')) || []).map(normalizeMod).filter(Boolean);
|
|
57
70
|
import { ensureHome, homeFile, PET_PORT, PKG_ROOT } from './paths.mjs';
|
|
58
71
|
import { applyEvent, loadPet, need, savePet, tick, view } from './pet-core.mjs';
|
|
59
72
|
|
|
@@ -95,6 +108,7 @@ export async function postPet(path, body, port = PET_PORT) {
|
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
const VOICE_PROVIDERS = ['elevenlabs', 'openai', 'apuchat', 'grok', 'fish'];
|
|
98
112
|
const SETTABLE_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ORQUESTA_TOKEN', 'ELEVENLABS_API_KEY', 'APUCHAT_VOICE_TOKEN', 'XAI_API_KEY', 'FISH_API_KEY', 'LETTA_API_KEY', 'APUMAIL_TOKEN'];
|
|
99
113
|
// The settings page speaks the human's language: SEVENOTS_LANG, config.lang, the system's, then the ot's.
|
|
100
114
|
const uiLangOf = (c, fallback) => [process.env.SEVENOTS_LANG, c.lang, String(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '').slice(0, 2).toLowerCase(), fallback].find((l) => ['en', 'es', 'pt'].includes(l)) || 'en';
|
|
@@ -107,11 +121,20 @@ const IDLE = { 0: Infinity, 1: 60 * 60_000, 2: 20 * 60_000, 3: 5 * 60_000 }; //
|
|
|
107
121
|
export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
108
122
|
const token = petToken();
|
|
109
123
|
let config = loadConfig();
|
|
124
|
+
let scenePlay = null; // { scene, at }: asked from settings, taken by the director on its next poll
|
|
125
|
+
const sceneConf = (c = config) => ({ on: true, every: 'normal', list: SCENE_NAMES, ...c.pet?.scenes });
|
|
110
126
|
const annoyOf = (c) => Math.max(0, Math.min(3, Number(c.pet.annoy ?? 2)));
|
|
111
127
|
let annoy = annoyOf(config);
|
|
112
128
|
const { identity } = loadIdentity();
|
|
113
129
|
if (!identity) throw Object.assign(new Error('no identity'), { code: 'NO_IDENTITY' });
|
|
114
130
|
const lang = identity.language || 'en';
|
|
131
|
+
/** The voice for a provider: what its settings card saved, else the identity's (only if it is that provider's). */
|
|
132
|
+
const voiceFor = (kind) => {
|
|
133
|
+
const iv = identity.voice || {};
|
|
134
|
+
const own = config.voice.providers?.[kind] || {};
|
|
135
|
+
// the identity's voice id/model belong to its own provider: not sent to another (an ElevenLabs id means nothing to Fish)
|
|
136
|
+
return { ...iv, ...(iv.provider === kind ? {} : { voiceId: '', model: '' }), provider: kind, ...(config.voice.voiceId ? { voiceId: config.voice.voiceId } : {}), ...own };
|
|
137
|
+
};
|
|
115
138
|
let brainImpl = createBrain(config.brain, config.home); // swapped live from the settings page
|
|
116
139
|
const inferences = createInferenceLog();
|
|
117
140
|
if (config.voice.kind === 'local') warmLocal();
|
|
@@ -130,6 +153,51 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
130
153
|
for (const res of clients) res.write(data);
|
|
131
154
|
};
|
|
132
155
|
|
|
156
|
+
// Approve / Reject in its bubble before acting for you (Orquesta…). Unanswered in 2 min = rejected.
|
|
157
|
+
const asks = new Map(); // id → { text, done }
|
|
158
|
+
function confirm(text, ms = 120_000) {
|
|
159
|
+
const id = randomBytes(6).toString('hex');
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const done = (ok) => {
|
|
162
|
+
if (!asks.delete(id)) return;
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
log(`approval ${ok ? 'yes' : 'no'}: ${text.slice(0, 80)}`);
|
|
165
|
+
broadcast({ ask: { id, done: true }, thinking: ok });
|
|
166
|
+
resolve(ok);
|
|
167
|
+
};
|
|
168
|
+
const timer = setTimeout(() => done(false), ms);
|
|
169
|
+
asks.set(id, { text, done });
|
|
170
|
+
lastSpoke = Date.now();
|
|
171
|
+
if (config.access.notify !== false) notify(`${identity.name} · 7ots`, text);
|
|
172
|
+
broadcast({ ask: { id, text }, thinking: false, gesture: 'think', point: 'user', stir: true });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Orquesta sign-in in the browser; once in, its integration is on
|
|
176
|
+
let connecting = null;
|
|
177
|
+
function connectOrquesta() {
|
|
178
|
+
return (connecting ||= import('./orquesta.mjs')
|
|
179
|
+
.then(async (o) => {
|
|
180
|
+
if (o.via() !== '7ots') return o.login({ open: openUrl });
|
|
181
|
+
// signed in to 7ots.com: connect it there (once for the ot everywhere); its token stays there
|
|
182
|
+
openUrl(`${SEVENOTS_URL}/app/#ots/${config.account.otsId}/orquesta`);
|
|
183
|
+
for (const end = Date.now() + 10 * 60_000; Date.now() < end; ) {
|
|
184
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
185
|
+
if ((await o.cloudOrquesta().catch(() => null))?.connected) return { organizationName: '' };
|
|
186
|
+
}
|
|
187
|
+
throw new Error('not connected on 7ots.com in time');
|
|
188
|
+
})
|
|
189
|
+
.then((r) => {
|
|
190
|
+
const next = loadConfig();
|
|
191
|
+
next.integrations = { ...next.integrations, orquesta: { ...next.integrations?.orquesta, on: true } };
|
|
192
|
+
saveConfig(next);
|
|
193
|
+
config = loadConfig();
|
|
194
|
+
log(`orquesta: connected${r.organizationName ? ` (${r.organizationName})` : ''}`);
|
|
195
|
+
broadcast({ orquesta: true });
|
|
196
|
+
return r;
|
|
197
|
+
})
|
|
198
|
+
.finally(() => (connecting = null)));
|
|
199
|
+
}
|
|
200
|
+
|
|
133
201
|
async function speak(kind, vars = {}, situation = '', { force = false, gesture = null } = {}) {
|
|
134
202
|
const now = Date.now();
|
|
135
203
|
if (!force && (pet.asleep || now - lastSpoke < GAP[annoy])) {
|
|
@@ -237,6 +305,28 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
237
305
|
broadcast({});
|
|
238
306
|
}, 60_000);
|
|
239
307
|
|
|
308
|
+
// Sync with its ot on 7ots.com (account.mjs sync): same look, mods, personality and sheet on both sides.
|
|
309
|
+
let syncing = false;
|
|
310
|
+
let syncSoon = null;
|
|
311
|
+
let syncErr = '';
|
|
312
|
+
async function cloudSync() {
|
|
313
|
+
if (syncing) return;
|
|
314
|
+
syncing = true;
|
|
315
|
+
try {
|
|
316
|
+
const r = await syncOt();
|
|
317
|
+
syncErr = '';
|
|
318
|
+
if (r.action === 'push' || r.action === 'pull') log(`sync: ${r.action} 7ots.com`);
|
|
319
|
+
if (r.action === 'pull') restart(); // its new look/personality, everywhere
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (e.message !== syncErr) log(`sync: ${(syncErr = e.message)}`);
|
|
322
|
+
} finally {
|
|
323
|
+
syncing = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const syncTimer = setInterval(cloudSync, 60_000);
|
|
327
|
+
const syncFirst = setTimeout(cloudSync, 8000);
|
|
328
|
+
const syncLater = () => (clearTimeout(syncSoon), (syncSoon = setTimeout(cloudSync, 3000)));
|
|
329
|
+
|
|
240
330
|
// Watchers: a price or a program it waits for, then it tells you (like a reminder, on a condition).
|
|
241
331
|
const watchers = createWatchers({
|
|
242
332
|
log,
|
|
@@ -253,6 +343,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
253
343
|
const assistant = createAssistant({
|
|
254
344
|
brain,
|
|
255
345
|
access: () => config.access,
|
|
346
|
+
memoryTokens: () => config.memory.tokens,
|
|
256
347
|
lang,
|
|
257
348
|
name: identity.name,
|
|
258
349
|
log,
|
|
@@ -266,12 +357,21 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
266
357
|
pet.lastTalk = lastSpoke;
|
|
267
358
|
broadcast({ say: text, gesture: 'wave', effect: 'exclaim', point: 'user', stir: pet.asleep });
|
|
268
359
|
},
|
|
360
|
+
// Prowl.world guides for sites mentioned (or, with access.screen, in the active window's title)
|
|
361
|
+
sites: createSiteContext({
|
|
362
|
+
enabled: () => config.access.sites !== false,
|
|
363
|
+
screen: () => config.access.screen === true,
|
|
364
|
+
windows: () => watcher?.seen?.windows || [],
|
|
365
|
+
log,
|
|
366
|
+
}),
|
|
269
367
|
// logs, email, Orquesta agents, custom integrations (settings → Permissions)
|
|
270
368
|
extra: createExtras({
|
|
271
369
|
config: () => config,
|
|
272
370
|
brain,
|
|
273
371
|
log,
|
|
274
372
|
system: (said) => petSystem(identity, { lang: said || 'auto', role: 'assistant' }),
|
|
373
|
+
confirm,
|
|
374
|
+
connect: connectOrquesta,
|
|
275
375
|
onLater: (text) => {
|
|
276
376
|
lastSpoke = Date.now();
|
|
277
377
|
pet.lastTalk = lastSpoke;
|
|
@@ -398,6 +498,12 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
398
498
|
if (!existsSync(f)) return res.writeHead(404).end('run npm run build');
|
|
399
499
|
return res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-cache' }).end(readFileSync(f));
|
|
400
500
|
}
|
|
501
|
+
if (req.method === 'GET' && /^\/mods\/[a-f0-9]{16}\.(vrm|glb)$/.test(p)) {
|
|
502
|
+
// 3D models of full-body mods, uploaded from settings (content-addressed, so cacheable)
|
|
503
|
+
const f = homeFile(p.slice(1));
|
|
504
|
+
if (!existsSync(f)) return res.writeHead(404).end();
|
|
505
|
+
return res.writeHead(200, { 'Content-Type': 'model/gltf-binary', 'Cache-Control': 'max-age=31536000, immutable' }).end(readFileSync(f));
|
|
506
|
+
}
|
|
401
507
|
if (req.method === 'GET' && p === '/favicon.svg') return res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'max-age=86400' }).end(readFileSync(join(PKG_ROOT, 'brand', 'favicon.svg')));
|
|
402
508
|
const authed = okToken(url.searchParams.get('token') || req.headers['x-7ots-token']);
|
|
403
509
|
if (!authed) return json(401, { error: 'token' });
|
|
@@ -440,12 +546,27 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
440
546
|
if (req.method === 'GET' && p === '/events') {
|
|
441
547
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
442
548
|
res.write(`data: ${JSON.stringify({ state: view(pet), ...(viewSet ? { view: viewSet } : {}) })}\n\n`);
|
|
549
|
+
for (const [id, a] of asks) res.write(`data: ${JSON.stringify({ ask: { id, text: a.text } })}\n\n`); // still waiting for you
|
|
443
550
|
clients.add(res);
|
|
444
551
|
req.on('close', () => clients.delete(res));
|
|
445
552
|
return;
|
|
446
553
|
}
|
|
447
|
-
if (req.method === 'GET' && p === '/
|
|
448
|
-
|
|
554
|
+
if (req.method === 'GET' && p === '/scenes') {
|
|
555
|
+
// polled by the desktop shell's director: how often ots play scenes, which ones, and a pending "play now"
|
|
556
|
+
const play = scenePlay;
|
|
557
|
+
scenePlay = null;
|
|
558
|
+
return json(200, { ...sceneConf(), play });
|
|
559
|
+
}
|
|
560
|
+
if (req.method === 'GET' && p === '/inferences') {
|
|
561
|
+
// this ot's calls plus those of the ots visiting this desktop (the host sees all), each tagged with its ot
|
|
562
|
+
const n = Number(url.searchParams.get('n')) || 100;
|
|
563
|
+
const me = { id: config.account?.otsId || 'local', name: identity.name || 'ot', self: true };
|
|
564
|
+
const all = [...inferences.list(MAX_INF).map((r) => ({ ...r, ot: me })), ...otherInferences()].sort((a, b) => b.at - a.at);
|
|
565
|
+
const ots = [...new Map(all.map((r) => [r.ot.id, r.ot])).values()];
|
|
566
|
+
const want = url.searchParams.get('ot');
|
|
567
|
+
return json(200, { ots, inferences: (want ? all.filter((r) => r.ot.id === want) : all).slice(0, n) });
|
|
568
|
+
}
|
|
569
|
+
if (req.method === 'GET' && p === '/memory') return json(200, { notes: assistant.notes, reminders: assistant.reminders, seen: watcher.seen, said: history, conversation: assistant.conversation, activity, pet: view(pet) });
|
|
449
570
|
if (req.method === 'GET' && p === '/account') {
|
|
450
571
|
const base = { url: SEVENOTS_URL, current: config.account || null, signedIn: signedIn() };
|
|
451
572
|
if (!base.signedIn) return json(200, base);
|
|
@@ -465,13 +586,14 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
465
586
|
if (req.method === 'GET' && p === '/reminders') return json(200, { reminders: assistant.reminders, notes: assistant.notes });
|
|
466
587
|
if (req.method === 'GET' && p === '/cloud') return json(200, await cloudStatus(config));
|
|
467
588
|
if (req.method === 'GET' && (p === '/orquesta/projects' || p === '/orquesta/prompt')) {
|
|
468
|
-
|
|
589
|
+
const o = await import('./orquesta.mjs');
|
|
590
|
+
if (!o.via()) return json(200, { connected: false });
|
|
469
591
|
try {
|
|
470
|
-
const o = await import('./orquesta.mjs');
|
|
471
592
|
if (p === '/orquesta/projects') return json(200, { connected: true, projects: await o.agentProjects() });
|
|
472
593
|
return json(200, { connected: true, ...(await o.agentTask(String(url.searchParams.get('id') || '').slice(0, 80))) });
|
|
473
594
|
} catch (e) {
|
|
474
|
-
|
|
595
|
+
const off = [401, 409].includes(e.status); // no token / not connected on 7ots.com
|
|
596
|
+
return json(off ? 200 : 502, { connected: !off, error: off ? undefined : e.message });
|
|
475
597
|
}
|
|
476
598
|
}
|
|
477
599
|
if (req.method !== 'POST') return json(405, { error: 'method' });
|
|
@@ -492,6 +614,23 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
492
614
|
return json(e.status || 502, { error: e.message });
|
|
493
615
|
}
|
|
494
616
|
}
|
|
617
|
+
if (p === '/mods/upload') {
|
|
618
|
+
// a .vrm/.glb for a full-body mod: stored in ~/.7ots/mods/<sha256-16>.<ext>
|
|
619
|
+
const chunks = [];
|
|
620
|
+
let n = 0;
|
|
621
|
+
for await (const c of req) {
|
|
622
|
+
if ((n += c.length) > 60 * 1024 * 1024) return json(413, { error: 'too large' });
|
|
623
|
+
chunks.push(c);
|
|
624
|
+
}
|
|
625
|
+
const buf = Buffer.concat(chunks);
|
|
626
|
+
if (buf.length < 20 || buf.toString('latin1', 0, 4) !== 'glTF') return json(400, { error: 'model' });
|
|
627
|
+
const ext = url.searchParams.get('ext') === 'vrm' ? 'vrm' : 'glb';
|
|
628
|
+
const name = `${createHash('sha256').update(buf).digest('hex').slice(0, 16)}.${ext}`;
|
|
629
|
+
mkdirSync(homeFile('mods'), { recursive: true });
|
|
630
|
+
writeFileSync(homeFile(`mods/${name}`), buf);
|
|
631
|
+
log(`mods: model ${name} ${Math.round(buf.length / 1024)}KB`);
|
|
632
|
+
return json(200, { ok: true, url: `/mods/${name}` });
|
|
633
|
+
}
|
|
495
634
|
let body = '';
|
|
496
635
|
for await (const c of req) if ((body += c).length > 64 * 1024) return json(413, { error: 'too large' });
|
|
497
636
|
let data = {};
|
|
@@ -500,6 +639,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
500
639
|
} catch {
|
|
501
640
|
return json(400, { error: 'json' });
|
|
502
641
|
}
|
|
642
|
+
if (p.startsWith('/identity/') || p === '/config') syncLater(); // a local change goes up to 7ots.com
|
|
503
643
|
if (p === '/event') {
|
|
504
644
|
onEvent(data);
|
|
505
645
|
savePet(pet);
|
|
@@ -516,7 +656,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
516
656
|
const { synthesize } = await import('../../server/tts.mjs');
|
|
517
657
|
// the identity's voice id/model belong to its own provider: not sent to another (an ElevenLabs id means nothing to Fish)
|
|
518
658
|
const iv = identity.voice || {};
|
|
519
|
-
const voice =
|
|
659
|
+
const voice = voiceFor(kind);
|
|
520
660
|
// process.env too: XAI_TTS_VOICE, FISH_VOICE_ID, APUCHAT_VOICE_URL… (the keys still come from env())
|
|
521
661
|
const { audio, contentType } = await synthesize({ text: String(data.text || '').slice(0, 600), voice, env: { ...process.env, ...env() } });
|
|
522
662
|
return res.writeHead(200, { 'Content-Type': contentType }).end(audio);
|
|
@@ -524,6 +664,20 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
524
664
|
return json(e.status || 502, { error: e.message });
|
|
525
665
|
}
|
|
526
666
|
}
|
|
667
|
+
if (p === '/voice/test') {
|
|
668
|
+
// try a provider's voice before saving it: { kind, voiceId, model, style, text }
|
|
669
|
+
const kind = String(data.kind || '');
|
|
670
|
+
if (!VOICE_PROVIDERS.includes(kind)) return json(400, { error: 'kind' });
|
|
671
|
+
try {
|
|
672
|
+
const { synthesize } = await import('../../server/tts.mjs');
|
|
673
|
+
const over = Object.fromEntries(['voiceId', 'model', 'style'].map((f) => [f, String(data[f] ?? '').trim().slice(0, 600)]));
|
|
674
|
+
const text = String(data.text || '').trim().slice(0, 300) || ({ es: `Hola, soy ${identity.name}. ¿Así me quieres oír?`, pt: `Oi, eu sou ${identity.name}. Quer me ouvir assim?` }[lang] || `Hi, I'm ${identity.name}. Is this how you want me to sound?`);
|
|
675
|
+
const { audio, contentType } = await synthesize({ text, voice: { ...voiceFor(kind), ...over }, env: { ...process.env, ...env() } });
|
|
676
|
+
return res.writeHead(200, { 'Content-Type': contentType }).end(audio);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
return json(e.status || 502, { error: e.message });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
527
681
|
if (p === '/ask') {
|
|
528
682
|
const text = String(data.text || '').trim().slice(0, 800);
|
|
529
683
|
if (!text) return json(400, { error: 'text' });
|
|
@@ -595,10 +749,88 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
595
749
|
}
|
|
596
750
|
return restart(); // identity, language and name are read once at start
|
|
597
751
|
}
|
|
752
|
+
if (p.startsWith('/community/')) {
|
|
753
|
+
// the 7ots.com gallery (server/platform/mods.mjs): browse without an account; share, rate and report with one
|
|
754
|
+
const act = p.slice('/community/'.length);
|
|
755
|
+
try {
|
|
756
|
+
if (act === 'list') return json(200, { signedIn: signedIn(), ...(await community.list(data)) });
|
|
757
|
+
if (!signedIn() && act !== 'install') return json(401, { error: 'login' });
|
|
758
|
+
if (act === 'mine') return json(200, await community.mine());
|
|
759
|
+
if (act === 'install') {
|
|
760
|
+
const mod = await community.install(data.id);
|
|
761
|
+
const [status, out] = changeMods({ mod });
|
|
762
|
+
return json(status, out);
|
|
763
|
+
}
|
|
764
|
+
if (act === 'rate') return json(200, await community.rate(data.id, data.stars));
|
|
765
|
+
if (act === 'report') return json(200, await community.report(data.id, data.reason));
|
|
766
|
+
if (act === 'remove') return json(200, await community.remove(data.id));
|
|
767
|
+
if (act === 'share') {
|
|
768
|
+
const mod = modLibrary().find((m) => m.id === data.id) || normalizeMods(identity.look?.character?.mods).find((m) => m.id === data.id);
|
|
769
|
+
if (!mod) return json(404, { error: 'mod' });
|
|
770
|
+
return json(200, await community.share(mod));
|
|
771
|
+
}
|
|
772
|
+
} catch (e) {
|
|
773
|
+
log(`community ${act}: ${e.message}`);
|
|
774
|
+
return json(e.status || 502, { error: e.message });
|
|
775
|
+
}
|
|
776
|
+
return json(404, { error: 'not found' });
|
|
777
|
+
}
|
|
778
|
+
if (p === '/identity/mods') {
|
|
779
|
+
const [status, out] = changeMods(data);
|
|
780
|
+
return json(status, out);
|
|
781
|
+
}
|
|
782
|
+
/** mods of the ot's look (a catalog one by id, or an imported .json); the pet reloads with them */
|
|
783
|
+
function changeMods(data) {
|
|
784
|
+
const { file } = loadIdentity();
|
|
785
|
+
const raw = readJson(file) || {};
|
|
786
|
+
const ch = raw.look?.character || {};
|
|
787
|
+
let mods = normalizeMods(ch.mods);
|
|
788
|
+
if (data.remove) mods = mods.filter((m) => m.id !== data.remove);
|
|
789
|
+
if (data.add || data.mod) {
|
|
790
|
+
const m = data.add ? MOD_CATALOG.find((c) => c.id === data.add) : data.mod;
|
|
791
|
+
const n = m && normalizeMod(m);
|
|
792
|
+
if (!n) return [400, { error: 'mod' }];
|
|
793
|
+
if (!mods.some((x) => x.id === n.id) && mods.length >= MAX_MODS) return [400, { error: 'maxMods' }];
|
|
794
|
+
// un muñeco completo reemplaza al ot entero: los accesorios 2D no se verían encima, así que
|
|
795
|
+
// poner uno quita el otro (si no, «poner orc» sobre un modelo no cambiaba nada)
|
|
796
|
+
mods = n.model ? [n] : [...mods.filter((x) => x.id !== n.id && !x.model), n];
|
|
797
|
+
// los importados quedan en la biblioteca local aunque después se quiten (o los reemplace otro)
|
|
798
|
+
if (data.mod) writeJson(homeFile('mods/library.json'), [...modLibrary().filter((x) => x.id !== n.id), n].slice(-60));
|
|
799
|
+
}
|
|
800
|
+
if (data.forget) writeJson(homeFile('mods/library.json'), modLibrary().filter((x) => x.id !== data.forget));
|
|
801
|
+
const look = { ...raw.look, character: { ...ch, mods } };
|
|
802
|
+
writeJson(file, { ...raw, look });
|
|
803
|
+
identity.look = defineIdentity({ ...raw, look }).look;
|
|
804
|
+
log(`identity: mods ${mods.map((m) => m.id).join(', ') || 'none'}`);
|
|
805
|
+
setTimeout(() => broadcast({ reload: true }), 100);
|
|
806
|
+
return [200, { ok: true, mods: mods.map((m) => m.id) }];
|
|
807
|
+
}
|
|
808
|
+
if (p === '/identity/lang') {
|
|
809
|
+
// the language the ot itself speaks (bubbles, brain, meet), kept in its identity file
|
|
810
|
+
if (!['en', 'es', 'pt'].includes(data.lang)) return json(400, { error: 'lang' });
|
|
811
|
+
const { file } = loadIdentity();
|
|
812
|
+
const raw = readJson(file) || {};
|
|
813
|
+
writeJson(file, { ...raw, language: data.lang, voice: { ...raw.voice, lang: { es: 'es-ES', en: 'en-US', pt: 'pt-BR' }[data.lang] } });
|
|
814
|
+
log(`identity: speaks ${data.lang}`);
|
|
815
|
+
json(200, { ok: true });
|
|
816
|
+
return restart();
|
|
817
|
+
}
|
|
598
818
|
if (p === '/inferences/clear') return inferences.clear(), json(200, { ok: true });
|
|
819
|
+
if (p === '/conversation/forget') return json(200, { ok: assistant.forgetConversation() });
|
|
599
820
|
if (p === '/notes/forget') return json(200, { ok: assistant.forget(data.at === 'all' ? 'all' : String(data.at ?? '')) });
|
|
600
821
|
if (p === '/watchers/cancel') return json(200, { ok: watchers.cancel(String(data.id || '')) });
|
|
601
822
|
if (p === '/reminders/cancel') return json(200, { ok: assistant.cancel(String(data.id || '')) });
|
|
823
|
+
if (p === '/approve') {
|
|
824
|
+
const a = asks.get(String(data.id || ''));
|
|
825
|
+
if (!a) return json(404, { error: 'gone' });
|
|
826
|
+
a.done(data.ok === true);
|
|
827
|
+
return json(200, { ok: true });
|
|
828
|
+
}
|
|
829
|
+
if (p === '/orquesta/connect') {
|
|
830
|
+
const r = connectOrquesta();
|
|
831
|
+
r.catch((e) => log(`orquesta: ${e.message}`));
|
|
832
|
+
return json(200, { ok: true });
|
|
833
|
+
}
|
|
602
834
|
if (p === '/orquesta/send') {
|
|
603
835
|
const projectId = String(data.projectId || '');
|
|
604
836
|
const text = String(data.text || '').trim();
|
|
@@ -609,6 +841,78 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
609
841
|
return json(e.status === 400 ? 400 : 502, { error: e.message });
|
|
610
842
|
}
|
|
611
843
|
}
|
|
844
|
+
if (p === '/voice/list') {
|
|
845
|
+
// the voices a provider offers with the saved key (ElevenLabs: yours + saved from the library; Fish: your models)
|
|
846
|
+
const kind = String(data.kind || '');
|
|
847
|
+
const keys = env();
|
|
848
|
+
try {
|
|
849
|
+
if (kind === 'elevenlabs') {
|
|
850
|
+
if (!keys.ELEVENLABS_API_KEY) return json(400, { error: 'key' });
|
|
851
|
+
const r = await fetch('https://api.elevenlabs.io/v1/voices', { headers: { 'xi-api-key': keys.ELEVENLABS_API_KEY }, signal: AbortSignal.timeout(15000) });
|
|
852
|
+
if (!r.ok) return json(502, { error: `ElevenLabs ${r.status}` });
|
|
853
|
+
const j = await r.json();
|
|
854
|
+
return json(200, { voices: (j.voices || []).map((v) => ({ id: v.voice_id, name: v.name, note: [v.category, v.labels?.accent, v.labels?.gender, v.labels?.age].filter(Boolean).join(' · '), preview: v.preview_url || '' })) });
|
|
855
|
+
}
|
|
856
|
+
if (kind === 'fish') {
|
|
857
|
+
if (!keys.FISH_API_KEY) return json(400, { error: 'key' });
|
|
858
|
+
const r = await fetch('https://api.fish.audio/model?self=true&page_size=50', { headers: { Authorization: `Bearer ${keys.FISH_API_KEY}` }, signal: AbortSignal.timeout(15000) });
|
|
859
|
+
if (!r.ok) return json(502, { error: `Fish Audio ${r.status}` });
|
|
860
|
+
const j = await r.json();
|
|
861
|
+
return json(200, { voices: (j.items || []).map((v) => ({ id: v._id, name: v.title, note: (v.languages || []).join(', ') })) });
|
|
862
|
+
}
|
|
863
|
+
if (kind === 'openai') return json(200, { voices: OPENAI_VOICES.map((v) => ({ id: v, name: v })) });
|
|
864
|
+
if (kind === 'grok') return json(200, { voices: GROK_VOICES.map((v) => ({ id: v, name: v })) });
|
|
865
|
+
return json(200, { voices: [] });
|
|
866
|
+
} catch (e) {
|
|
867
|
+
return json(502, { error: e.message });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (p === '/character/draft') {
|
|
871
|
+
// the brain drafts the character sheet from the identity (+ an optional idea); nothing is saved
|
|
872
|
+
if (brain.kind === 'lines') return json(400, { error: 'brain' });
|
|
873
|
+
const cur = config.character || {};
|
|
874
|
+
const KEYS = ['backstory', 'style', 'quirks', 'scenario', 'greeting', 'examples'];
|
|
875
|
+
const who = { name: identity.name, role: identity.role, tagline: identity.tagline, bio: identity.bio, tone: identity.personality?.tone, traits: identity.personality?.traits, look: (identity.look?.character?.mods || []).map((m) => m.id) };
|
|
876
|
+
const L = ['es', 'en', 'pt'].includes(data.lang) ? data.lang : lang;
|
|
877
|
+
const LN = { es: 'Spanish (español)', pt: 'Brazilian Portuguese (português)', en: 'English' }[L];
|
|
878
|
+
const FIELDS = {
|
|
879
|
+
backstory: 'backstory (4-6 sentences: where it comes from, what it wants, what it fears)',
|
|
880
|
+
style: 'style (how it talks: rhythm, words it likes, what it never does)',
|
|
881
|
+
quirks: 'quirks (5-8 short habits, one per line)',
|
|
882
|
+
scenario: 'scenario (1-2 sentences: where it lives and what it does all day)',
|
|
883
|
+
greeting: 'greeting (one short line)',
|
|
884
|
+
examples: `examples (3-4 tiny exchanges, lines «Human: …» and «${identity.name}: …»)`,
|
|
885
|
+
};
|
|
886
|
+
const ask = (keys) => `Write a character sheet for a desktop pet called ${identity.name}.
|
|
887
|
+
LANGUAGE: write every field entirely in ${LN}, even if the profile below is in another language (translate it).
|
|
888
|
+
Who it is (from its profile): ${JSON.stringify(who)}
|
|
889
|
+
${KEYS.some((k) => cur[k]) ? `Current sheet (keep what is good, improve the rest): ${JSON.stringify(Object.fromEntries(KEYS.map((k) => [k, cur[k] || ''])))}` : ''}
|
|
890
|
+
${data.idea ? `The owner's idea for it: «${String(data.idea).slice(0, 600)}»` : ''}
|
|
891
|
+
Make it varied and specific so it never gets stuck on one or two topics (it is a companion that comments on the owner's coding work, chats and keeps them company): many interests, opinions, running jokes, small habits.
|
|
892
|
+
Answer ONLY a flat JSON object with exactly these keys, each a non-empty string in ${LN}:
|
|
893
|
+
${keys.map((k) => FIELDS[k]).join('\n')}`;
|
|
894
|
+
// models drift: other casing, arrays, nested objects or a wrapper ({ sheet: {…} })
|
|
895
|
+
const textOf = (v) => (Array.isArray(v) ? v.map(textOf).join('\n') : v && typeof v === 'object' ? Object.entries(v).map(([k, x]) => (typeof x === 'object' ? textOf(x) : /^(human|user|q|a|\d+)$/i.test(k) ? x : `${k}: ${x}`)).join('\n') : String(v ?? ''));
|
|
896
|
+
const pick = (j) => {
|
|
897
|
+
const o = j && typeof j === 'object' ? (Object.keys(j).length === 1 && typeof Object.values(j)[0] === 'object' && !Array.isArray(Object.values(j)[0]) ? Object.values(j)[0] : j) : {};
|
|
898
|
+
const low = Object.fromEntries(Object.entries(o).map(([k, v]) => [k.toLowerCase().replace(/[^a-z]/g, ''), v]));
|
|
899
|
+
const alias = { backstory: ['backstory', 'history', 'background', 'story', 'description'], style: ['style', 'speakingstyle', 'voice'], quirks: ['quirks', 'habits'], scenario: ['scenario', 'setting'], greeting: ['greeting', 'firstmessage', 'hello'], examples: ['examples', 'exampledialogue', 'dialogues', 'exchanges'] };
|
|
900
|
+
return Object.fromEntries(KEYS.map((k) => [k, textOf(alias[k].map((a) => low[a]).find((v) => v != null && v !== '') ?? '').trim()]));
|
|
901
|
+
};
|
|
902
|
+
const run = async (keys) => {
|
|
903
|
+
const raw = String(await brain.think({ purpose: 'character', system: `You are a character writer. You write in ${LN}. Output strict JSON only.`, messages: [{ role: 'user', content: ask(keys) }], timeoutMs: 120000, raw: true }));
|
|
904
|
+
return pick(JSON.parse(raw.slice(raw.indexOf('{'), raw.lastIndexOf('}') + 1)));
|
|
905
|
+
};
|
|
906
|
+
try {
|
|
907
|
+
const out = await run(KEYS);
|
|
908
|
+
const missing = KEYS.filter((k) => !out[k]);
|
|
909
|
+
if (missing.length) Object.assign(out, Object.fromEntries(Object.entries(await run(missing).catch(() => ({}))).filter(([k, v]) => missing.includes(k) && v)));
|
|
910
|
+
const lim = { backstory: 3000, style: 1500, quirks: 1500, scenario: 1500, greeting: 600, examples: 4000 };
|
|
911
|
+
return json(200, { lang: L, sheet: Object.fromEntries(KEYS.map((k) => [k, (out[k] || '').slice(0, lim[k])])) });
|
|
912
|
+
} catch (e) {
|
|
913
|
+
return json(502, { error: e.message });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
612
916
|
if (p === '/character/letta') {
|
|
613
917
|
try {
|
|
614
918
|
const r = await lettaSync(identity, config.character);
|
|
@@ -623,6 +927,10 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
623
927
|
return json(e.status === 400 ? 400 : 502, { error: e.message });
|
|
624
928
|
}
|
|
625
929
|
}
|
|
930
|
+
if (p === '/scenes/play') {
|
|
931
|
+
scenePlay = { scene: SCENE_NAMES.includes(data.scene) ? data.scene : '', at: Date.now() };
|
|
932
|
+
return json(200, { ok: true });
|
|
933
|
+
}
|
|
626
934
|
if (p === '/quit') {
|
|
627
935
|
json(200, { ok: true });
|
|
628
936
|
await Promise.all(fullOnes().map(quitCompanion)); // the second ot leaves with it (a restart keeps it)
|
|
@@ -637,7 +945,8 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
637
945
|
return {
|
|
638
946
|
name: identity.name,
|
|
639
947
|
lang,
|
|
640
|
-
|
|
948
|
+
visitor: !!process.env.SEVENOTS_COMPANION, // a visiting ot: the host's settings rule shared things (scenes)
|
|
949
|
+
config: { brain: config.brain, pet: { ...config.pet, scenes: sceneConf() }, access: config.access, voice: config.voice, screen: config.screen, memory: config.memory },
|
|
641
950
|
brain: brainLabel(config.brain),
|
|
642
951
|
clis: detectClis(),
|
|
643
952
|
stt: sttProvider(keys),
|
|
@@ -645,6 +954,10 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
645
954
|
voices: { local: localVoices(), keys: { elevenlabs: Boolean(keys.ELEVENLABS_API_KEY), openai: Boolean(keys.OPENAI_API_KEY), apuchat: Boolean(keys.APUCHAT_VOICE_TOKEN), grok: Boolean(keys.XAI_API_KEY), fish: Boolean(keys.FISH_API_KEY) } },
|
|
646
955
|
uiLang: uiLangOf(config, lang),
|
|
647
956
|
configLang: config.lang || '',
|
|
957
|
+
otLook: identity.look?.character || null,
|
|
958
|
+
// the catalog without its 3D models (the page only draws the 2D previews)
|
|
959
|
+
modLibrary: modLibrary(),
|
|
960
|
+
modCatalog: MOD_CATALOG.map((m) => normalizeMod(m)).map((m) => ({ ...m, parts: m.parts.map((x) => ({ ...x, glb: x.glb ? '3d' : '' })) })),
|
|
648
961
|
integrations: config.integrations,
|
|
649
962
|
character: config.character,
|
|
650
963
|
account: config.account || null,
|
|
@@ -685,6 +998,14 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
685
998
|
if (d.pet) {
|
|
686
999
|
if (d.pet.annoy !== undefined) next.pet.annoy = Math.max(0, Math.min(3, Math.round(Number(d.pet.annoy)) || 0));
|
|
687
1000
|
if (['2d', '3d'].includes(d.pet.view)) next.pet.view = d.pet.view;
|
|
1001
|
+
if (d.pet.scenes) {
|
|
1002
|
+
const c = { ...sceneConf(next), ...d.pet.scenes };
|
|
1003
|
+
next.pet.scenes = {
|
|
1004
|
+
on: c.on !== false,
|
|
1005
|
+
every: SCENE_EVERY.includes(c.every) ? c.every : 'normal',
|
|
1006
|
+
list: Array.isArray(c.list) ? [...new Set(c.list.filter((n) => SCENE_NAMES.includes(n)))] : SCENE_NAMES,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
688
1009
|
}
|
|
689
1010
|
if (d.access) for (const k of Object.keys(next.access)) if (typeof d.access[k] === 'boolean') next.access[k] = d.access[k];
|
|
690
1011
|
if (Array.isArray(d.access?.vmKinds)) next.access.vmKinds = [...new Set(d.access.vmKinds.filter((k) => VM_KINDS.includes(k)))];
|
|
@@ -698,12 +1019,29 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
698
1019
|
} catch (e) {
|
|
699
1020
|
return e.message;
|
|
700
1021
|
}
|
|
1022
|
+
if (d.memory?.tokens !== undefined) {
|
|
1023
|
+
const n = Math.round(Number(d.memory.tokens)) || 0;
|
|
1024
|
+
next.memory.tokens = n <= 0 ? 0 : Math.max(2000, Math.min(200000, n));
|
|
1025
|
+
}
|
|
701
1026
|
if (d.screen?.every !== undefined) next.screen.every = Math.max(1, Math.min(60, Math.round(Number(d.screen.every)) || 5));
|
|
702
1027
|
let reload = false;
|
|
703
1028
|
if (d.voice) {
|
|
704
1029
|
if (!['none', 'browser', 'local', 'elevenlabs', 'openai', 'apuchat', 'grok', 'fish'].includes(d.voice.kind)) return 'voice';
|
|
705
1030
|
reload = d.voice.kind !== next.voice.kind;
|
|
706
1031
|
next.voice = { ...next.voice, kind: d.voice.kind };
|
|
1032
|
+
// each provider's own voice, model and style ({ elevenlabs: { voiceId, model }, openai: { voiceId, model, style } … })
|
|
1033
|
+
const pv = d.voice.providers;
|
|
1034
|
+
if (pv && typeof pv === 'object') {
|
|
1035
|
+
const providers = { ...(next.voice.providers || {}) };
|
|
1036
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
1037
|
+
if (!VOICE_PROVIDERS.includes(k) || !v || typeof v !== 'object') continue;
|
|
1038
|
+
const clean = Object.fromEntries(['voiceId', 'model', 'style', 'name'].map((f) => [f, String(v[f] ?? '').replace(/[\u0000-\u001f]/g, '').trim().slice(0, f === 'style' ? 600 : 120)]).filter(([, x]) => x));
|
|
1039
|
+
if (Object.keys(clean).length) providers[k] = clean;
|
|
1040
|
+
else delete providers[k];
|
|
1041
|
+
}
|
|
1042
|
+
next.voice.providers = providers;
|
|
1043
|
+
reload = true;
|
|
1044
|
+
}
|
|
707
1045
|
}
|
|
708
1046
|
if (d.keys) {
|
|
709
1047
|
for (const [k, v] of Object.entries(d.keys)) {
|
|
@@ -736,6 +1074,9 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
736
1074
|
|
|
737
1075
|
function stop(then) {
|
|
738
1076
|
clearInterval(timer);
|
|
1077
|
+
clearInterval(syncTimer);
|
|
1078
|
+
clearTimeout(syncFirst);
|
|
1079
|
+
clearTimeout(syncSoon);
|
|
739
1080
|
assistant.stop();
|
|
740
1081
|
watchers.stop();
|
|
741
1082
|
stopWatch();
|