@7ots/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -1
- package/cli/7ots.mjs +133 -36
- package/cli/lib/assistant.mjs +337 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/i18n.mjs +78 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +30 -0
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +79 -28
- package/cli/pet/pet.html +55 -6
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- package/src/index.js +1 -0
package/.env.example
CHANGED
|
@@ -125,9 +125,21 @@ PLATFORM_DB=./data/platform.db
|
|
|
125
125
|
PLATFORM_SECRET=
|
|
126
126
|
PLATFORM_MAX_OTS=10
|
|
127
127
|
PLATFORM_OTS_RATE_PER_MIN=120
|
|
128
|
+
# Llamadas de chat y voz por ots y día (tope de gasto aunque alguien falsifique el Origin)
|
|
129
|
+
PLATFORM_OTS_DAILY_LIMIT=1000
|
|
130
|
+
# Cuentas nuevas por IP y día (cada una trae cupo gratis)
|
|
131
|
+
PLATFORM_NEW_ACCOUNTS_PER_IP_DAY=3
|
|
132
|
+
# Llamadas pagadas por la plataforma en curso a la vez por cuenta
|
|
133
|
+
PLATFORM_MAX_INFLIGHT=4
|
|
128
134
|
# IA y voz de la plataforma para los ots sin clave propia, con cupo gratis por cuenta y mes
|
|
135
|
+
# PLATFORM_LLM_PROVIDER: anthropic | openai (y compatibles: DeepSeek, Groq… con PLATFORM_LLM_BASE_URL)
|
|
136
|
+
PLATFORM_LLM_PROVIDER=
|
|
137
|
+
PLATFORM_LLM_API_KEY=
|
|
138
|
+
PLATFORM_LLM_BASE_URL=
|
|
139
|
+
PLATFORM_LLM_MODEL=
|
|
140
|
+
# Ejemplo DeepSeek: PLATFORM_LLM_PROVIDER=openai PLATFORM_LLM_BASE_URL=https://api.deepseek.com PLATFORM_LLM_MODEL=deepseek-flash
|
|
141
|
+
# (compatibilidad: PLATFORM_ANTHROPIC_API_KEY=… equivale a provider anthropic con esa clave)
|
|
129
142
|
PLATFORM_ANTHROPIC_API_KEY=
|
|
130
|
-
PLATFORM_LLM_MODEL=claude-sonnet-5-5
|
|
131
143
|
PLATFORM_FREE_MESSAGES=100
|
|
132
144
|
PLATFORM_APUCHAT_VOICE_TOKEN=
|
|
133
145
|
PLATFORM_FREE_TTS=100
|
|
@@ -138,3 +150,7 @@ PLATFORM_MAIL_TOKEN=
|
|
|
138
150
|
NOTLOGIN_URL=https://notlogin.com
|
|
139
151
|
NOTLOGIN_CLIENT_ID=
|
|
140
152
|
NOTLOGIN_CLIENT_SECRET=
|
|
153
|
+
# Identidades gratis de apuchat por cuenta y día (pestaña apuchat de cada ots; el hub es APUCHAT_HUB)
|
|
154
|
+
PLATFORM_APUCHAT_IDENTITIES_PER_DAY=5
|
|
155
|
+
# Orquesta: tareas que un ots puede encargar a un agente (OAuth con registro dinámico, sin client id fijo)
|
|
156
|
+
ORQUESTA_URL=https://getorquesta.com
|
package/cli/7ots.mjs
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* `7ots help` lists the commands. Texts are English by default (es/pt: SEVENOTS_LANG, config or LANG).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
8
|
-
import { openSync } from 'node:fs';
|
|
7
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
8
|
+
import { existsSync, mkdirSync, openSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { createIdentity, describe, identityPrompt, loadIdentity, cleanSeed } from './lib/identity.mjs';
|
|
11
12
|
import { loadConfig } from './lib/config.mjs';
|
|
@@ -14,8 +15,8 @@ import { ensureHome, homeFile, PET_PORT, PKG_ROOT } from './lib/paths.mjs';
|
|
|
14
15
|
import { applyEvent, loadPet, savePet, tick, view } from './lib/pet-core.mjs';
|
|
15
16
|
import { petAlive, petToken, postPet, startPetServer } from './lib/pet-server.mjs';
|
|
16
17
|
import { fromClaudeHook, installClaudeHooks, installShellHooks, redact, removeClaudeHooks, removeShellHooks } from './lib/hooks.mjs';
|
|
17
|
-
import { findHtml, installHtml, platformLink, pretty, snippet } from './lib/install.mjs';
|
|
18
|
-
import { promptText } from './lib/prompts.mjs';
|
|
18
|
+
import { detectProject, findHtml, installHtml, platformLink, pretty, snippet } from './lib/install.mjs';
|
|
19
|
+
import { llmsTxt, promptText } from './lib/prompts.mjs';
|
|
19
20
|
import { openUrl, graphical } from './lib/open.mjs';
|
|
20
21
|
|
|
21
22
|
const ELECTRON = 'electron@37';
|
|
@@ -42,7 +43,7 @@ function parse(argv) {
|
|
|
42
43
|
}
|
|
43
44
|
return { pos, flags };
|
|
44
45
|
}
|
|
45
|
-
const VALUED = new Set(['seed', 'name', 'lang', 'target', 'mode', 'type', 'code', 'cmd', 'tool', 'from', 'port', 'text', 'minutes']);
|
|
46
|
+
const VALUED = new Set(['seed', 'name', 'lang', 'target', 'mode', 'type', 'code', 'cmd', 'tool', 'from', 'port', 'text', 'minutes', 'cancel']);
|
|
46
47
|
|
|
47
48
|
const fail = (msg, code = 1) => {
|
|
48
49
|
console.error(msg);
|
|
@@ -85,7 +86,14 @@ function cmdInstall({ pos, flags }) {
|
|
|
85
86
|
const block = snippet(identity, { home });
|
|
86
87
|
const html = target === 'print' ? null : pos[0] || findHtml();
|
|
87
88
|
if (!html) {
|
|
88
|
-
|
|
89
|
+
const proj = detectProject();
|
|
90
|
+
if (proj.kind === 'none' && !pos[0] && target !== 'print') {
|
|
91
|
+
// Not a website: the snippet would be useless here; say what the ot can do instead.
|
|
92
|
+
console.log(t('install.notWeb', { name: identity.name, app }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const where = proj.file ? t('install.pasteIn', { file: proj.file, name: proj.name || '' }) : t('install.noHtml');
|
|
96
|
+
console.log(where + '\n\n' + block + '\n');
|
|
89
97
|
} else {
|
|
90
98
|
const r = installHtml(html, block);
|
|
91
99
|
console.log(t(r.updated ? 'install.htmlUpdated' : 'install.html', { file: pretty(html) }));
|
|
@@ -131,42 +139,99 @@ async function spawnDaemon() {
|
|
|
131
139
|
fail(`the pet did not start; see ${homeFile('pet.log')}`);
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
function petUrl(desktop = false) {
|
|
135
|
-
return `http://127.0.0.1:${PET_PORT}/pet?${desktop ? 'desktop=1&' : ''}token=${petToken()}`;
|
|
142
|
+
function petUrl(desktop = false, view3d = false) {
|
|
143
|
+
return `http://127.0.0.1:${PET_PORT}/pet?${desktop ? 'desktop=1&' : ''}${view3d ? '3d=1&' : ''}token=${petToken()}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Path to the Electron binary, or null. `electron`'s index.js throws if its postinstall download never finished. */
|
|
147
|
+
function electronBin(dir) {
|
|
148
|
+
try {
|
|
149
|
+
const bin = createRequire(join(dir, 'package.json'))('electron');
|
|
150
|
+
return typeof bin === 'string' && existsSync(bin) ? bin : null;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Electron lives in ~/.7ots/electron, installed once with npm in the foreground. Not `npx electron`:
|
|
158
|
+
* if npx is cut off mid-download (we used to detach it and close its pipes) its cache keeps a package
|
|
159
|
+
* without the binary and every later run fails with "electron: not found".
|
|
160
|
+
*/
|
|
161
|
+
function ensureElectron() {
|
|
162
|
+
const dir = homeFile('electron');
|
|
163
|
+
let bin = electronBin(dir);
|
|
164
|
+
if (bin) return { bin };
|
|
165
|
+
ensureHome();
|
|
166
|
+
console.log(dim(t('pet.electronInstall', { dir })));
|
|
167
|
+
if (!existsSync(join(dir, 'package.json'))) {
|
|
168
|
+
mkdirSync(dir, { recursive: true });
|
|
169
|
+
writeFileSync(join(dir, 'package.json'), '{ "name": "7ots-electron", "private": true }\n');
|
|
170
|
+
}
|
|
171
|
+
const r = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error', ELECTRON], {
|
|
172
|
+
cwd: dir,
|
|
173
|
+
stdio: 'inherit',
|
|
174
|
+
shell: process.platform === 'win32',
|
|
175
|
+
});
|
|
176
|
+
bin = electronBin(dir);
|
|
177
|
+
return bin ? { bin } : { error: r.error?.message || `npm install ${ELECTRON} failed (exit ${r.status})` };
|
|
136
178
|
}
|
|
137
179
|
|
|
138
180
|
/** Electron window (detached). Resolves ok once it has stayed up for a few seconds. */
|
|
139
|
-
function openDesktop() {
|
|
181
|
+
function openDesktop({ opaque = false, view3d = false } = {}) {
|
|
140
182
|
console.log(dim(t('pet.electron')));
|
|
141
|
-
const
|
|
142
|
-
|
|
183
|
+
const { bin, error } = ensureElectron();
|
|
184
|
+
if (!bin) return Promise.resolve({ ok: false, error });
|
|
185
|
+
const log = openSync(homeFile('electron.log'), 'a');
|
|
186
|
+
const child = spawn(bin, [join(PKG_ROOT, 'cli', 'pet', 'electron', 'main.cjs')], {
|
|
187
|
+
stdio: ['ignore', log, log],
|
|
143
188
|
detached: true,
|
|
144
|
-
env: { ...process.env, SEVENOTS_PET_URL: `http://127.0.0.1:${PET_PORT}`, SEVENOTS_PET_TOKEN: petToken() },
|
|
145
|
-
shell: process.platform === 'win32',
|
|
189
|
+
env: { ...process.env, SEVENOTS_PET_URL: `http://127.0.0.1:${PET_PORT}`, SEVENOTS_PET_TOKEN: petToken(), SEVENOTS_PET_OPAQUE: opaque ? '1' : '', SEVENOTS_PET_3D: view3d ? '1' : '', SEVENOTS_PET_LANG: cliLang() },
|
|
146
190
|
});
|
|
147
191
|
return new Promise((resolve) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
child.unref();
|
|
160
|
-
resolve({ ok: true });
|
|
161
|
-
}
|
|
162
|
-
}, 500);
|
|
163
|
-
child.once('exit', (code) => {
|
|
164
|
-
clearInterval(timer);
|
|
165
|
-
resolve(code ? { ok: false, error: (err.trim().split('\n').pop() || `exit ${code}`).slice(0, 160) } : { ok: true });
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
child.unref();
|
|
194
|
+
resolve({ ok: true });
|
|
195
|
+
}, 3000);
|
|
196
|
+
child.once('error', (e) => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
resolve({ ok: false, error: e.message });
|
|
199
|
+
});
|
|
200
|
+
child.once('exit', (code, sig) => {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
resolve({ ok: false, error: `exit ${code ?? sig}; see ${homeFile('electron.log')}` });
|
|
166
203
|
});
|
|
167
204
|
});
|
|
168
205
|
}
|
|
169
206
|
|
|
207
|
+
/** Talk to the pet: reminders, notes, opening sites (the daemon's /ask). */
|
|
208
|
+
async function cmdAsk({ pos }) {
|
|
209
|
+
const text = pos.join(' ').trim();
|
|
210
|
+
if (!text) fail(t('usage.ask'));
|
|
211
|
+
if (!(await petAlive())) fail(t('err.noPet'));
|
|
212
|
+
const r = await fetch(`http://127.0.0.1:${PET_PORT}/ask`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'Content-Type': 'application/json', 'X-7ots-Token': petToken() },
|
|
215
|
+
body: JSON.stringify({ text }),
|
|
216
|
+
signal: AbortSignal.timeout(60_000),
|
|
217
|
+
})
|
|
218
|
+
.then((x) => x.json())
|
|
219
|
+
.catch((e) => fail(e.message));
|
|
220
|
+
console.log(r.reply || '');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function cmdReminders({ flags }) {
|
|
224
|
+
if (!(await petAlive())) fail(t('err.noPet'));
|
|
225
|
+
if (flags.cancel) {
|
|
226
|
+
const ok = await postPet('/reminders/cancel', { id: String(flags.cancel) });
|
|
227
|
+
return console.log(ok ? 'ok' : '?');
|
|
228
|
+
}
|
|
229
|
+
const r = await fetch(`http://127.0.0.1:${PET_PORT}/reminders?token=${petToken()}`).then((x) => x.json());
|
|
230
|
+
for (const x of r.reminders) console.log(`${dim(x.id)} ${new Date(x.at).toLocaleString()} ${x.text}${x.open ? dim(` → ${x.open}`) : ''}`);
|
|
231
|
+
if (!r.reminders.length) console.log(dim('—'));
|
|
232
|
+
if (r.notes.length) console.log(`\n${bold('notes')}\n${r.notes.map((n) => ` · ${n.text}`).join('\n')}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
170
235
|
async function cmdPet({ flags }) {
|
|
171
236
|
if (flags.daemon) {
|
|
172
237
|
try {
|
|
@@ -180,8 +245,10 @@ async function cmdPet({ flags }) {
|
|
|
180
245
|
}
|
|
181
246
|
|
|
182
247
|
const { identity } = needIdentity();
|
|
248
|
+
if (flags.tmux) return petInTmux(identity, flags);
|
|
183
249
|
const mode = await chooseMode(identity, flags);
|
|
184
250
|
const detach = !!flags.detach;
|
|
251
|
+
const view3d = !!flags['3d'] || loadConfig().pet.view === '3d';
|
|
185
252
|
|
|
186
253
|
let alive = await petAlive();
|
|
187
254
|
let local = null;
|
|
@@ -197,24 +264,35 @@ async function cmdPet({ flags }) {
|
|
|
197
264
|
if (mode === 'terminal') {
|
|
198
265
|
const { runTerminal } = await import('./lib/terminal.mjs');
|
|
199
266
|
const annoy = Number(loadConfig().pet.annoy ?? 2);
|
|
200
|
-
await runTerminal({ identity, lang:
|
|
267
|
+
await runTerminal({ identity, lang: cliLang(), port: PET_PORT, bell: annoy >= 3, compact: !!flags.compact || (process.stdout.columns || 80) < 44 });
|
|
201
268
|
if (local) local.stop();
|
|
202
269
|
return;
|
|
203
270
|
}
|
|
204
271
|
|
|
205
272
|
if (mode === 'desktop') {
|
|
206
|
-
const r = await openDesktop();
|
|
273
|
+
const r = await openDesktop({ opaque: !!flags.opaque || !!loadConfig().pet.opaque, view3d });
|
|
207
274
|
if (!r.ok) {
|
|
208
275
|
console.log(t('pet.electronFail', { error: r.error }));
|
|
209
|
-
openUrl(petUrl());
|
|
210
|
-
}
|
|
276
|
+
openUrl(petUrl(false, view3d));
|
|
277
|
+
} else console.log(t('pet.where', { name: identity.name }));
|
|
211
278
|
} else {
|
|
212
|
-
openUrl(petUrl());
|
|
279
|
+
openUrl(petUrl(false, view3d));
|
|
213
280
|
console.log(t('pet.running', { name: identity.name, url: `http://127.0.0.1:${PET_PORT}/pet` }));
|
|
214
281
|
}
|
|
215
282
|
console.log(t('pet.detached', { name: identity.name }));
|
|
216
283
|
}
|
|
217
284
|
|
|
285
|
+
/** The terminal pet in a tmux pane next to this one (the daemon lives on its own, so the pane can close). */
|
|
286
|
+
async function petInTmux(identity, flags) {
|
|
287
|
+
if (!process.env.TMUX) fail(t('pet.tmuxNo'));
|
|
288
|
+
if (!(await petAlive())) await spawnDaemon();
|
|
289
|
+
const cmd = [process.execPath, join(PKG_ROOT, 'cli', '7ots.mjs'), 'pet', '--mode', 'terminal', ...(flags.compact ? ['--compact'] : [])]
|
|
290
|
+
.map((a) => `'${String(a).replace(/'/g, `'\\''`)}'`)
|
|
291
|
+
.join(' ');
|
|
292
|
+
const r = spawn('tmux', ['split-window', '-h', '-d', '-l', flags.compact ? '28' : '40', cmd], { stdio: 'inherit' });
|
|
293
|
+
r.once('exit', (code) => (code ? fail('tmux split-window failed') : console.log(t('pet.tmuxOk', { name: identity.name }))));
|
|
294
|
+
}
|
|
295
|
+
|
|
218
296
|
/** Events from hooks. Never fails loudly: a hook must not break the agent or the shell. */
|
|
219
297
|
async function cmdEvent({ flags }) {
|
|
220
298
|
let ev = null;
|
|
@@ -262,6 +340,11 @@ async function cmdStatus({ flags = {} } = {}) {
|
|
|
262
340
|
} catch {}
|
|
263
341
|
state ||= view(tick(loadPet()));
|
|
264
342
|
if (flags.json) return console.log(JSON.stringify(state, null, 2));
|
|
343
|
+
if (flags.line) {
|
|
344
|
+
// Short and plain: made for the tmux status bar.
|
|
345
|
+
const low = Math.min(state.food, state.energy, state.fun) < 25 ? '!' : '';
|
|
346
|
+
return console.log(`${identity?.name || '7ots'}${state.asleep ? ' z' : ''} ♥${state.food} ⚡${state.energy} ★${state.fun}${low}`);
|
|
347
|
+
}
|
|
265
348
|
console.log(
|
|
266
349
|
t('pet.status', {
|
|
267
350
|
name: identity?.name || '7ots',
|
|
@@ -336,6 +419,12 @@ async function main() {
|
|
|
336
419
|
const [cmd = 'help', ...rest] = process.argv.slice(2);
|
|
337
420
|
const args = parse(rest);
|
|
338
421
|
if (args.flags.lang && ['en', 'es', 'pt'].includes(args.flags.lang)) (await import('./lib/i18n.mjs')).setCliLang(args.flags.lang);
|
|
422
|
+
// `7ots <cmd> --help` only prints usage: it never launches anything (the pet would download Electron).
|
|
423
|
+
if (args.flags.help && !['help', '--help', '-h'].includes(cmd)) {
|
|
424
|
+
const key = { create: 'new', serve: 'server', wizard: 'setup', init: 'setup' }[cmd] || cmd;
|
|
425
|
+
const usage = t(`usage.${key}`);
|
|
426
|
+
return console.log(usage === `usage.${key}` ? t('help') : usage);
|
|
427
|
+
}
|
|
339
428
|
switch (cmd) {
|
|
340
429
|
case 'new':
|
|
341
430
|
case 'create':
|
|
@@ -363,6 +452,11 @@ async function main() {
|
|
|
363
452
|
case 'wake':
|
|
364
453
|
case 'say':
|
|
365
454
|
return cmdCare(cmd, args);
|
|
455
|
+
case 'ask':
|
|
456
|
+
case 'tell':
|
|
457
|
+
return cmdAsk(args);
|
|
458
|
+
case 'reminders':
|
|
459
|
+
return cmdReminders(args);
|
|
366
460
|
case 'status':
|
|
367
461
|
return cmdStatus(args);
|
|
368
462
|
case 'stop':
|
|
@@ -374,6 +468,9 @@ async function main() {
|
|
|
374
468
|
case 'prompt':
|
|
375
469
|
case 'prompts':
|
|
376
470
|
return console.log(promptText(args.pos[0] || 'all'));
|
|
471
|
+
case 'docs':
|
|
472
|
+
case 'llms':
|
|
473
|
+
return console.log(llmsTxt('https://7ots.com'));
|
|
377
474
|
case 'server':
|
|
378
475
|
case 'serve':
|
|
379
476
|
return import('../server/server.mjs'); // the 7ots proxy (reads .env / env vars)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pet as a small daily assistant: you tell it things and it acts.
|
|
3
|
+
*
|
|
4
|
+
* "recuérdame ver el correo en 10 minutos" → reminder; when due it speaks, notifies and opens Gmail
|
|
5
|
+
* "remind me at 15:30 to call Ana" → reminder
|
|
6
|
+
* "anota que los viernes hay demo" → a note it remembers (and the AI brain reads)
|
|
7
|
+
* "abre el calendario" · "mis recordatorios" · "cancela los recordatorios" · "qué sabes de mí"
|
|
8
|
+
*
|
|
9
|
+
* With an AI brain (config.brain) the AI reads the request and answers with JSON actions; with
|
|
10
|
+
* `lines` (or if the AI fails) a local parser understands the common phrasings in en/es/pt.
|
|
11
|
+
* State: ~/.7ots/reminders.json and ~/.7ots/memory.json.
|
|
12
|
+
* Coming soon: seeing the screen / the running processes (opt-in) to suggest and do things.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
import { homeFile, readJson, writeJson } from './paths.mjs';
|
|
18
|
+
import { openUrl } from './open.mjs';
|
|
19
|
+
|
|
20
|
+
const SITES = [
|
|
21
|
+
[/\b(correo|mail|gmail|e-?mail|inbox|bandeja)\b/i, 'https://mail.google.com', { en: 'your email', es: 'tu correo', pt: 'seu e-mail' }],
|
|
22
|
+
[/\b(calendario|calendar|agenda|calend[aá]rio)\b/i, 'https://calendar.google.com', { en: 'your calendar', es: 'tu calendario', pt: 'sua agenda' }],
|
|
23
|
+
[/\bwhats ?app\b/i, 'https://web.whatsapp.com', { en: 'WhatsApp', es: 'WhatsApp', pt: 'WhatsApp' }],
|
|
24
|
+
[/\bslack\b/i, 'https://app.slack.com', { en: 'Slack', es: 'Slack', pt: 'Slack' }],
|
|
25
|
+
[/\bgithub\b/i, 'https://github.com', { en: 'GitHub', es: 'GitHub', pt: 'GitHub' }],
|
|
26
|
+
[/\b(drive)\b/i, 'https://drive.google.com', { en: 'Drive', es: 'Drive', pt: 'Drive' }],
|
|
27
|
+
[/\blinked ?in\b/i, 'https://www.linkedin.com', { en: 'LinkedIn', es: 'LinkedIn', pt: 'LinkedIn' }],
|
|
28
|
+
[/\b(twitter|x\.com)\b/i, 'https://x.com', { en: 'X', es: 'X', pt: 'X' }],
|
|
29
|
+
[/\b(google meet|meet)\b/i, 'https://meet.google.com', { en: 'Meet', es: 'Meet', pt: 'Meet' }],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const T = {
|
|
33
|
+
en: {
|
|
34
|
+
when: 'When? For example "in 10 minutes" or "at 15:30".',
|
|
35
|
+
set: (r) => `Got it: I'll remind you to ${r.text} at ${hhmm(r.at)}${r.open ? ' and open it for you' : ''}.`,
|
|
36
|
+
none: 'No pending reminders.',
|
|
37
|
+
list: (rs) => `Pending: ${rs.map((r) => `${hhmm(r.at)} ${r.text}`).join(' · ')}`,
|
|
38
|
+
cancelled: (n) => `Cancelled ${n} reminder${n === 1 ? '' : 's'}.`,
|
|
39
|
+
noted: 'Noted, I will remember it.',
|
|
40
|
+
know: (ns) => (ns.length ? `I remember: ${ns.map((n) => n.text).join(' · ')}` : "I don't know much about you yet. Tell me \"remember that…\"."),
|
|
41
|
+
opening: (s) => `Opening ${s}.`,
|
|
42
|
+
fire: (r) => `Reminder: ${r.text}!`,
|
|
43
|
+
help: 'I can set reminders ("remind me to check email in 10 minutes"), open sites ("open my calendar") and remember notes ("remember that…"). Seeing your screen: coming soon.',
|
|
44
|
+
},
|
|
45
|
+
es: {
|
|
46
|
+
when: '¿Para cuándo? Por ejemplo "en 10 minutos" o "a las 15:30".',
|
|
47
|
+
set: (r) => `Listo: te recuerdo ${r.text} a las ${hhmm(r.at)}${r.open ? ' y te lo abro' : ''}.`,
|
|
48
|
+
none: 'No tienes recordatorios pendientes.',
|
|
49
|
+
list: (rs) => `Pendientes: ${rs.map((r) => `${hhmm(r.at)} ${r.text}`).join(' · ')}`,
|
|
50
|
+
cancelled: (n) => `Cancelé ${n} recordatorio${n === 1 ? '' : 's'}.`,
|
|
51
|
+
noted: 'Anotado, lo voy a recordar.',
|
|
52
|
+
know: (ns) => (ns.length ? `Recuerdo: ${ns.map((n) => n.text).join(' · ')}` : 'Todavía sé poco de ti. Dime "anota que…".'),
|
|
53
|
+
opening: (s) => `Abriendo ${s}.`,
|
|
54
|
+
fire: (r) => `¡Recordatorio: ${r.text}!`,
|
|
55
|
+
help: 'Puedo poner recordatorios ("recuérdame ver el correo en 10 minutos"), abrir sitios ("abre mi calendario") y recordar notas ("anota que…"). Ver tu pantalla: muy pronto.',
|
|
56
|
+
},
|
|
57
|
+
pt: {
|
|
58
|
+
when: 'Para quando? Por exemplo "em 10 minutos" ou "às 15:30".',
|
|
59
|
+
set: (r) => `Feito: te lembro de ${r.text} às ${hhmm(r.at)}${r.open ? ' e abro pra você' : ''}.`,
|
|
60
|
+
none: 'Nenhum lembrete pendente.',
|
|
61
|
+
list: (rs) => `Pendentes: ${rs.map((r) => `${hhmm(r.at)} ${r.text}`).join(' · ')}`,
|
|
62
|
+
cancelled: (n) => `Cancelei ${n} lembrete${n === 1 ? '' : 's'}.`,
|
|
63
|
+
noted: 'Anotado, vou lembrar.',
|
|
64
|
+
know: (ns) => (ns.length ? `Eu lembro: ${ns.map((n) => n.text).join(' · ')}` : 'Ainda sei pouco de você. Diga "anote que…".'),
|
|
65
|
+
opening: (s) => `Abrindo ${s}.`,
|
|
66
|
+
fire: (r) => `Lembrete: ${r.text}!`,
|
|
67
|
+
help: 'Posso criar lembretes ("me lembre de ver o e-mail em 10 minutos"), abrir sites ("abra minha agenda") e lembrar notas ("anote que…"). Ver sua tela: em breve.',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The language the human wrote in (replies follow it), or null. */
|
|
72
|
+
export function langOf(text) {
|
|
73
|
+
const s = String(text || '').toLowerCase();
|
|
74
|
+
if (W('[ãõç]|\\b(me lembre|lembre|lembrete|voc[eê]|abra minha|anote|meus|daqui a|amanh[aã])\\b', 'u').test(s)) return 'pt';
|
|
75
|
+
if (W('[ñ¿¡]|\\b(recu[eé]rd|recordame|av[ií]same|anota|apunta|abre|abr[ií]|mis|qu[eé]|ma[nñ]ana|minutos|a las|correo|cancela)\\b', 'u').test(s)) return 'es';
|
|
76
|
+
if (/\b(remind|remember|open|my|what|tomorrow|minutes?|at \d)\b/.test(s)) return 'en';
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hhmm = (at) => new Date(at).toTimeString().slice(0, 5);
|
|
81
|
+
const tr = (lang) => T[lang] || T.en;
|
|
82
|
+
|
|
83
|
+
// ── local parser ──
|
|
84
|
+
|
|
85
|
+
// \b does not see accented letters (é, í, ã, à) as word characters: unicode-aware boundaries.
|
|
86
|
+
const B = '(?<![\\p{L}\\d])';
|
|
87
|
+
const E = '(?![\\p{L}\\d])';
|
|
88
|
+
const W = (src, flags = 'iu') => new RegExp(src.replaceAll('\\b(', `${B}(`).replaceAll(')\\b', `)${E}`), flags);
|
|
89
|
+
|
|
90
|
+
const NUM = { un: 1, una: 1, uno: 1, uma: 1, um: 1, a: 1, an: 1, one: 1, dos: 2, two: 2, 'dois': 2, tres: 3, three: 3, 'três': 3, cinco: 5, five: 5, diez: 10, ten: 10, dez: 10, quince: 15, fifteen: 15, quinze: 15, veinte: 20, twenty: 20, vinte: 20, treinta: 30, thirty: 30, trinta: 30 };
|
|
91
|
+
const UNIT = (u) => (/^(s|seg|sec)/.test(u) ? 1000 : /^(h|hr|hora|hour)/.test(u) ? 3_600_000 : 60_000);
|
|
92
|
+
const IN_RE = W('\\b(?:en|in|em|dentro de|daqui a)\\s+(\\d+|un|una|uno|uma|um|an?|one|dos|two|dois|tres|three|três|cinco|five|diez|ten|dez|quince|fifteen|quinze|veinte|twenty|vinte|treinta|thirty|trinta)\\s*(segundos?|seg|secs?|seconds?|s|minutos?|mins?|minutes?|m|horas?|hours?|hrs?|h)\\b');
|
|
93
|
+
const HALF_RE = W('\\b(?:en|in|em|dentro de)\\s+(?:media hora|half an hour|meia hora)\\b');
|
|
94
|
+
const AT_RE = W('\\b(?:a las|a la|at|às|as)\\s+(\\d{1,2})(?:[:.h](\\d{2}))?\\s*(am|pm|hrs?|horas?)?\\b');
|
|
95
|
+
const TOMORROW_RE = W('\\b(mañana|manana|tomorrow|amanh[aã])\\b');
|
|
96
|
+
const REMIND_RE = W('\\b(recu[eé]rd[aá]me|recordame|recu[eé]rdame|av[ií]same|remind me|me lembr[ae]|lembre-me|lembra-me)\\b');
|
|
97
|
+
const NOTE_RE = W('^\\s*(?:anota(?:\\s+que)?|apunta(?:\\s+que)?|recuerda que|aprende que|remember(?:\\s+that)?|note(?:\\s+that)?|anote(?:\\s+que)?|lembre que|lembra que)\\s+');
|
|
98
|
+
const OPEN_RE = W('^\\s*(?:abre|abr[ií]|abrime|[aá]breme|open|abra|abrir)\\b');
|
|
99
|
+
const LIST_RE = W('\\b(mis recordatorios|qu[eé] recordatorios|recordatorios pendientes|my reminders|list reminders|reminders|meus lembretes|lembretes)\\b');
|
|
100
|
+
const CANCEL_RE = W('\\b(cancela|borra|elimina|cancel|clear|apaga|remove)\\b.*\\b(recordatorios?|reminders?|lembretes?)\\b');
|
|
101
|
+
const KNOW_RE = W('\\b(qu[eé] (?:sabes|recuerdas)|what do you (?:know|remember)|o que (?:voc[eê] )?(?:sabe|lembra))\\b');
|
|
102
|
+
const HELP_RE = W('^\\s*(ayuda|help|ajuda|qu[eé] puedes hacer|what can you do|o que voc[eê] pode fazer)\\s*\\??\\s*$');
|
|
103
|
+
|
|
104
|
+
function siteOf(text) {
|
|
105
|
+
const url = text.match(/https?:\/\/[^\s]+/i)?.[0];
|
|
106
|
+
if (url) return { url, label: url };
|
|
107
|
+
for (const [re, u, label] of SITES) if (re.test(text)) return { url: u, label };
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** When, from a phrase. Returns epoch ms or null. */
|
|
112
|
+
export function parseWhen(text, now = Date.now()) {
|
|
113
|
+
if (HALF_RE.test(text)) return now + 30 * 60_000;
|
|
114
|
+
const m = text.match(IN_RE);
|
|
115
|
+
if (m) {
|
|
116
|
+
const n = /^\d+$/.test(m[1]) ? Number(m[1]) : NUM[m[1].toLowerCase()] || 1;
|
|
117
|
+
return now + n * UNIT(m[2].toLowerCase());
|
|
118
|
+
}
|
|
119
|
+
const a = text.match(AT_RE);
|
|
120
|
+
const tomorrow = TOMORROW_RE.test(text);
|
|
121
|
+
if (a) {
|
|
122
|
+
let h = Number(a[1]);
|
|
123
|
+
const min = Number(a[2] || 0);
|
|
124
|
+
if (/pm/i.test(a[3] || '') && h < 12) h += 12;
|
|
125
|
+
if (/am/i.test(a[3] || '') && h === 12) h = 0;
|
|
126
|
+
if (h > 23 || min > 59) return null;
|
|
127
|
+
const d = new Date(now);
|
|
128
|
+
d.setHours(h, min, 0, 0);
|
|
129
|
+
if (tomorrow) d.setDate(d.getDate() + 1);
|
|
130
|
+
else if (d.getTime() <= now) d.setDate(d.getDate() + 1); // "a las 9" said at 10 → tomorrow
|
|
131
|
+
return d.getTime();
|
|
132
|
+
}
|
|
133
|
+
if (tomorrow) {
|
|
134
|
+
const d = new Date(now);
|
|
135
|
+
d.setDate(d.getDate() + 1);
|
|
136
|
+
d.setHours(9, 0, 0, 0);
|
|
137
|
+
return d.getTime();
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** What to remember, without the trigger and the time words. */
|
|
143
|
+
function reminderText(text) {
|
|
144
|
+
const s = text
|
|
145
|
+
.replace(REMIND_RE, ' ')
|
|
146
|
+
.replace(HALF_RE, ' ')
|
|
147
|
+
.replace(IN_RE, ' ')
|
|
148
|
+
.replace(AT_RE, ' ')
|
|
149
|
+
.replace(TOMORROW_RE, ' ')
|
|
150
|
+
.replace(/\by (?:que )?(?:t[uú] mismo |él mismo |el mismo |ella misma )?(?:me )?(?:abras?|abre|abr[ií]|open|abra)\b.*$/i, ' ') // "…y que me abras la pestaña"
|
|
151
|
+
.replace(/\band (?:then )?open\b.*$/i, ' ')
|
|
152
|
+
.replace(/^\s*(?:de |que |to |that |para )/i, ' ')
|
|
153
|
+
.replace(/\s+/g, ' ')
|
|
154
|
+
.replace(/^[\s,.:;-]+|[\s,.:;!?-]+$/g, '')
|
|
155
|
+
.replace(/^(?:de|que|to|that|para)\s+/i, '');
|
|
156
|
+
return s || text.trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Local understanding → list of actions. */
|
|
160
|
+
export function parseLocal(text, now = Date.now()) {
|
|
161
|
+
const s = String(text || '').trim();
|
|
162
|
+
if (!s) return [];
|
|
163
|
+
if (HELP_RE.test(s)) return [{ type: 'help' }];
|
|
164
|
+
if (CANCEL_RE.test(s)) return [{ type: 'cancel' }];
|
|
165
|
+
if (LIST_RE.test(s) && !REMIND_RE.test(s)) return [{ type: 'list' }];
|
|
166
|
+
if (KNOW_RE.test(s)) return [{ type: 'know' }];
|
|
167
|
+
if (NOTE_RE.test(s)) return [{ type: 'note', text: s.replace(NOTE_RE, '').trim() }];
|
|
168
|
+
const site = siteOf(s);
|
|
169
|
+
const at = parseWhen(s, now);
|
|
170
|
+
if (REMIND_RE.test(s) || at) {
|
|
171
|
+
if (!at) return [{ type: 'ask_when' }];
|
|
172
|
+
return [{ type: 'remind', at, text: reminderText(s), open: site?.url || null }];
|
|
173
|
+
}
|
|
174
|
+
if (OPEN_RE.test(s) && site) return [{ type: 'open', url: site.url, label: site.label }];
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── AI understanding ──
|
|
179
|
+
|
|
180
|
+
function aiPrompt(text, { lang, notes, reminders, now }) {
|
|
181
|
+
return [
|
|
182
|
+
`Your human wrote to you: """${String(text).slice(0, 800)}"""`,
|
|
183
|
+
`Now it is ${new Date(now).toString()}.`,
|
|
184
|
+
notes.length ? `What you know about them: ${notes.map((n) => `- ${n.text}`).join('\n')}` : '',
|
|
185
|
+
reminders.length ? `Pending reminders: ${reminders.map((r) => `${new Date(r.at).toString()}: ${r.text}`).join('; ')}` : '',
|
|
186
|
+
`You can act. Answer ONLY with one JSON object, no prose around it:`,
|
|
187
|
+
`{"reply":"one short line in character, in the human's language","actions":[...]}`,
|
|
188
|
+
`Actions: {"type":"remind","in_minutes":N | "at":"ISO-8601 local time","text":"what to remind (short, imperative)","open":"https://… or null"}`,
|
|
189
|
+
`{"type":"open","url":"https://…"} (open now) · {"type":"note","text":"a durable fact or preference worth remembering"} · {"type":"cancel"} (all pending reminders) · {"type":"list"}`,
|
|
190
|
+
`When a reminder is about a website (email → https://mail.google.com, calendar → https://calendar.google.com, etc.) set "open" so it opens when due.`,
|
|
191
|
+
`If they share something about their work, routine or preferences, add a note. If a reminder has no time, ask when in "reply" with no actions.`,
|
|
192
|
+
`Reply in ${{ es: 'Spanish', pt: 'Portuguese' }[lang] || 'English'}.`,
|
|
193
|
+
]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseAi(raw, now) {
|
|
199
|
+
const m = String(raw || '').match(/\{[\s\S]*\}/);
|
|
200
|
+
if (!m) return null;
|
|
201
|
+
let j;
|
|
202
|
+
try {
|
|
203
|
+
j = JSON.parse(m[0]);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const actions = [];
|
|
208
|
+
for (const a of Array.isArray(j.actions) ? j.actions : []) {
|
|
209
|
+
if (a?.type === 'remind') {
|
|
210
|
+
const at = Number.isFinite(Number(a.in_minutes)) && a.in_minutes !== null && a.in_minutes !== '' ? now + Number(a.in_minutes) * 60_000 : Date.parse(a.at);
|
|
211
|
+
if (Number.isFinite(at) && at > now - 60_000 && a.text) actions.push({ type: 'remind', at, text: String(a.text).slice(0, 200), open: safeUrl(a.open) });
|
|
212
|
+
} else if (a?.type === 'open' && safeUrl(a.url)) actions.push({ type: 'open', url: safeUrl(a.url), label: a.url });
|
|
213
|
+
else if (a?.type === 'note' && a.text) actions.push({ type: 'note', text: String(a.text).slice(0, 300) });
|
|
214
|
+
else if (['cancel', 'list'].includes(a?.type)) actions.push({ type: a.type });
|
|
215
|
+
}
|
|
216
|
+
return { reply: String(j.reply || '').slice(0, 400), actions };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const safeUrl = (u) => (typeof u === 'string' && /^https?:\/\/[^\s"'<>]+$/i.test(u) ? u : null);
|
|
220
|
+
|
|
221
|
+
// ── the assistant ──
|
|
222
|
+
|
|
223
|
+
/** Desktop notification, best effort. */
|
|
224
|
+
function notify(title, body) {
|
|
225
|
+
const [cmd, args] =
|
|
226
|
+
process.platform === 'darwin'
|
|
227
|
+
? ['osascript', ['-e', `display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}`]]
|
|
228
|
+
: process.platform === 'win32'
|
|
229
|
+
? [null, []]
|
|
230
|
+
: ['notify-send', ['-a', '7ots', '-u', 'critical', title, body]];
|
|
231
|
+
if (!cmd) return;
|
|
232
|
+
try {
|
|
233
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {{ brain, system: () => string, lang: string, name: string, onFire: (reminder, line) => void, log?: Function }} o
|
|
239
|
+
*/
|
|
240
|
+
export function createAssistant({ brain, system, lang, name, onFire, log = () => {} }) {
|
|
241
|
+
let L = tr(lang);
|
|
242
|
+
const remF = homeFile('reminders.json');
|
|
243
|
+
const memF = homeFile('memory.json');
|
|
244
|
+
let reminders = (readJson(remF, []) || []).filter((r) => r && !r.done);
|
|
245
|
+
let notes = readJson(memF, []) || [];
|
|
246
|
+
const saveR = () => writeJson(remF, reminders);
|
|
247
|
+
const saveN = () => writeJson(memF, notes.slice(-200));
|
|
248
|
+
|
|
249
|
+
function fire(r) {
|
|
250
|
+
r.done = true;
|
|
251
|
+
const line = tr(r.lang || lang).fire(r);
|
|
252
|
+
notify(`${name} · 7ots`, r.text);
|
|
253
|
+
if (r.open) openUrl(r.open);
|
|
254
|
+
onFire(r, line);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// A minute late is fine; missed while the daemon was down → fire on start (once).
|
|
258
|
+
const timer = setInterval(() => {
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
let changed = false;
|
|
261
|
+
for (const r of reminders) if (!r.done && r.at <= now) (fire(r), (changed = true));
|
|
262
|
+
if (changed) {
|
|
263
|
+
reminders = reminders.filter((r) => !r.done);
|
|
264
|
+
saveR();
|
|
265
|
+
}
|
|
266
|
+
}, 5000);
|
|
267
|
+
timer.unref?.();
|
|
268
|
+
|
|
269
|
+
function run(actions) {
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const a of actions) {
|
|
272
|
+
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() };
|
|
274
|
+
reminders.push(r);
|
|
275
|
+
reminders.sort((x, y) => x.at - y.at);
|
|
276
|
+
saveR();
|
|
277
|
+
out.push(L.set(r));
|
|
278
|
+
} else if (a.type === 'open') {
|
|
279
|
+
openUrl(a.url);
|
|
280
|
+
out.push(L.opening(typeof a.label === 'object' ? a.label[L === T.es ? 'es' : L === T.pt ? 'pt' : 'en'] : a.label));
|
|
281
|
+
} else if (a.type === 'note') {
|
|
282
|
+
notes.push({ text: a.text, at: Date.now() });
|
|
283
|
+
saveN();
|
|
284
|
+
out.push(L.noted);
|
|
285
|
+
} else if (a.type === 'cancel') {
|
|
286
|
+
const n = reminders.length;
|
|
287
|
+
reminders = [];
|
|
288
|
+
saveR();
|
|
289
|
+
out.push(L.cancelled(n));
|
|
290
|
+
} 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)));
|
|
292
|
+
else if (a.type === 'ask_when') out.push(L.when);
|
|
293
|
+
else if (a.type === 'help') out.push(L.help);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
get reminders() {
|
|
300
|
+
return reminders.slice();
|
|
301
|
+
},
|
|
302
|
+
get notes() {
|
|
303
|
+
return notes.slice();
|
|
304
|
+
},
|
|
305
|
+
/** Free text → { reply, actions }. Never throws. */
|
|
306
|
+
async ask(text) {
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
const said = langOf(text) || lang;
|
|
309
|
+
L = tr(said);
|
|
310
|
+
const local = parseLocal(text, now);
|
|
311
|
+
let ai = null;
|
|
312
|
+
if (brain.kind !== 'lines') {
|
|
313
|
+
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 });
|
|
315
|
+
ai = parseAi(raw, now);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
log(`assistant: ${e.message}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (ai && (ai.actions.length || !local.length)) {
|
|
321
|
+
const done = run(ai.actions);
|
|
322
|
+
return { reply: ai.reply || done.join(' ') || L.help, actions: ai.actions };
|
|
323
|
+
}
|
|
324
|
+
const done = run(local);
|
|
325
|
+
return { reply: done.join(' ') || L.help, actions: local };
|
|
326
|
+
},
|
|
327
|
+
cancel(id) {
|
|
328
|
+
const n = reminders.length;
|
|
329
|
+
reminders = reminders.filter((r) => r.id !== id);
|
|
330
|
+
saveR();
|
|
331
|
+
return n !== reminders.length;
|
|
332
|
+
},
|
|
333
|
+
stop() {
|
|
334
|
+
clearInterval(timer);
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|