neoagent 1.4.0 → 1.4.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 +5 -0
- package/com.neoagent.plist +8 -6
- package/docs/configuration.md +9 -1
- package/docs/skills.md +6 -2
- package/lib/manager.js +37 -10
- package/package.json +4 -1
- package/runtime/paths.js +80 -0
- package/server/db/database.js +78 -4
- package/server/index.js +5 -5
- package/server/public/app.html +124 -49
- package/server/public/assets/world-office-dark.png +0 -0
- package/server/public/assets/world-office-light.png +0 -0
- package/server/public/css/app.css +575 -242
- package/server/public/css/styles.css +445 -121
- package/server/public/js/app.js +1041 -423
- package/server/routes/memory.js +3 -1
- package/server/routes/settings.js +42 -6
- package/server/routes/skills.js +124 -84
- package/server/routes/store.js +102 -1
- package/server/services/ai/compaction.js +15 -31
- package/server/services/ai/engine.js +224 -202
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/providers/google.js +8 -1
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -98
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +26 -7
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +158 -5
- package/server/services/browser/controller.js +124 -48
- package/server/services/manager.js +26 -3
- package/server/services/mcp/client.js +1 -1
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +211 -17
- package/server/services/messaging/telnyx.js +3 -2
- package/server/services/messaging/whatsapp.js +3 -2
- package/server/services/scheduler/cron.js +6 -1
- package/server/services/websocket.js +19 -6
package/.env.example
CHANGED
|
@@ -26,3 +26,8 @@ OPENAI_API_KEY=your-openai-api-key-here
|
|
|
26
26
|
# • Gemini models (e.g. gemini-3.1-flash-lite-preview)
|
|
27
27
|
# Get your key at: https://aistudio.google.com/app/apikey
|
|
28
28
|
GOOGLE_AI_KEY=your-google-ai-key-here
|
|
29
|
+
|
|
30
|
+
# Brave Search API key — used for:
|
|
31
|
+
# • Native web_search tool (search the web without driving the browser)
|
|
32
|
+
# Get your key at: https://api.search.brave.com/
|
|
33
|
+
BRAVE_SEARCH_API_KEY=your-brave-search-api-key-here
|
package/com.neoagent.plist
CHANGED
|
@@ -7,21 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
<key>ProgramArguments</key>
|
|
9
9
|
<array>
|
|
10
|
-
<string
|
|
11
|
-
<string
|
|
10
|
+
<string>__NODE_BIN__</string>
|
|
11
|
+
<string>__APP_DIR__/server/index.js</string>
|
|
12
12
|
</array>
|
|
13
13
|
|
|
14
14
|
<key>WorkingDirectory</key>
|
|
15
|
-
<string
|
|
15
|
+
<string>__APP_DIR__</string>
|
|
16
16
|
|
|
17
17
|
<key>EnvironmentVariables</key>
|
|
18
18
|
<dict>
|
|
19
19
|
<key>PATH</key>
|
|
20
20
|
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
21
21
|
<key>HOME</key>
|
|
22
|
-
<string
|
|
22
|
+
<string>__HOME__</string>
|
|
23
23
|
<key>NODE_ENV</key>
|
|
24
24
|
<string>production</string>
|
|
25
|
+
<key>NEOAGENT_HOME</key>
|
|
26
|
+
<string>__RUNTIME_HOME__</string>
|
|
25
27
|
</dict>
|
|
26
28
|
|
|
27
29
|
<!-- Auto-restart if the process exits for any reason -->
|
|
@@ -37,9 +39,9 @@
|
|
|
37
39
|
<integer>10</integer>
|
|
38
40
|
|
|
39
41
|
<key>StandardOutPath</key>
|
|
40
|
-
<string
|
|
42
|
+
<string>__LOG_DIR__/neoagent.log</string>
|
|
41
43
|
|
|
42
44
|
<key>StandardErrorPath</key>
|
|
43
|
-
<string
|
|
45
|
+
<string>__LOG_DIR__/neoagent.error.log</string>
|
|
44
46
|
</dict>
|
|
45
47
|
</plist>
|
package/docs/configuration.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Configuration
|
|
2
2
|
|
|
3
|
-
All settings live in
|
|
3
|
+
All settings live in `~/.neoagent/.env` by default. Run `neoagent setup` to regenerate interactively.
|
|
4
|
+
You can override the runtime root with `NEOAGENT_HOME`.
|
|
4
5
|
|
|
5
6
|
## Variables
|
|
6
7
|
|
|
@@ -24,6 +25,7 @@ At least one API key is required. The active provider and model are configured i
|
|
|
24
25
|
| `OPENAI_API_KEY` | GPT-4o / Whisper (OpenAI) |
|
|
25
26
|
| `XAI_API_KEY` | Grok (xAI) |
|
|
26
27
|
| `GOOGLE_AI_KEY` | Gemini (Google) |
|
|
28
|
+
| `BRAVE_SEARCH_API_KEY` | Brave Search API for the native `web_search` tool |
|
|
27
29
|
| `OLLAMA_URL` | Local Ollama (`http://localhost:11434`) |
|
|
28
30
|
|
|
29
31
|
### Messaging
|
|
@@ -34,6 +36,12 @@ At least one API key is required. The active provider and model are configured i
|
|
|
34
36
|
|
|
35
37
|
Telegram, Discord, and WhatsApp tokens are stored in the database via the web UI Settings page — not in `.env`.
|
|
36
38
|
|
|
39
|
+
## Runtime data paths
|
|
40
|
+
|
|
41
|
+
- Config: `~/.neoagent/.env`
|
|
42
|
+
- Database/session/logs: `~/.neoagent/data/`
|
|
43
|
+
- Skills/soul/daily memory files: `~/.neoagent/agent-data/`
|
|
44
|
+
|
|
37
45
|
---
|
|
38
46
|
|
|
39
47
|
## Minimal `.env` example
|
package/docs/skills.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skills
|
|
2
2
|
|
|
3
|
-
Skills are Markdown files in
|
|
3
|
+
Skills are Markdown files in `~/.neoagent/agent-data/skills/` by default. They are loaded at runtime — no restart needed after editing.
|
|
4
4
|
|
|
5
5
|
## Built-in skills
|
|
6
6
|
|
|
@@ -23,10 +23,14 @@ Skills are Markdown files in `agent-data/skills/` that describe capabilities the
|
|
|
23
23
|
| `tail-log.md` | Tail any log file |
|
|
24
24
|
| `news-hackernews.md` | Fetch Hacker News top stories |
|
|
25
25
|
| `qr-code.md` | Generate QR codes |
|
|
26
|
+
| `pdf-toolkit.md` | Inspect, extract, merge, split, and compress PDF files |
|
|
27
|
+
| `git-summary.md` | Summarize git status, branches, commits, and diffs |
|
|
28
|
+
| `csv-toolkit.md` | Inspect and transform CSV/TSV data files |
|
|
29
|
+
| `markdown-workbench.md` | Clean up, outline, and convert Markdown notes/docs |
|
|
26
30
|
|
|
27
31
|
## Adding a skill
|
|
28
32
|
|
|
29
|
-
Create a Markdown file in
|
|
33
|
+
Create a Markdown file in `~/.neoagent/agent-data/skills/`:
|
|
30
34
|
|
|
31
35
|
```markdown
|
|
32
36
|
# My Skill Name
|
package/lib/manager.js
CHANGED
|
@@ -5,15 +5,22 @@ const net = require('net');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
|
+
const {
|
|
9
|
+
APP_DIR,
|
|
10
|
+
RUNTIME_HOME,
|
|
11
|
+
DATA_DIR,
|
|
12
|
+
LOG_DIR,
|
|
13
|
+
ENV_FILE,
|
|
14
|
+
PID_FILE,
|
|
15
|
+
ensureRuntimeDirs,
|
|
16
|
+
migrateLegacyRuntime
|
|
17
|
+
} = require('../runtime/paths');
|
|
8
18
|
|
|
9
|
-
const APP_DIR = path.resolve(__dirname, '..');
|
|
10
19
|
const APP_NAME = 'NeoAgent';
|
|
11
20
|
const SERVICE_LABEL = 'com.neoagent';
|
|
12
21
|
const PLIST_SRC = path.join(APP_DIR, 'com.neoagent.plist');
|
|
13
22
|
const PLIST_DST = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.neoagent.plist');
|
|
14
23
|
const SYSTEMD_UNIT = path.join(os.homedir(), '.config', 'systemd', 'user', 'neoagent.service');
|
|
15
|
-
const LOG_DIR = path.join(APP_DIR, 'data', 'logs');
|
|
16
|
-
const ENV_FILE = path.join(APP_DIR, '.env');
|
|
17
24
|
|
|
18
25
|
const COLORS = process.stdout.isTTY
|
|
19
26
|
? {
|
|
@@ -136,7 +143,18 @@ function commandExists(cmd) {
|
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
function ensureLogDir() {
|
|
139
|
-
|
|
146
|
+
ensureRuntimeDirs();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function backupRuntimeData() {
|
|
150
|
+
const backupsDir = path.join(RUNTIME_HOME, 'backups');
|
|
151
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
152
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
153
|
+
const target = path.join(backupsDir, `pre-update-${stamp}`);
|
|
154
|
+
fs.mkdirSync(target, { recursive: true });
|
|
155
|
+
|
|
156
|
+
if (fs.existsSync(ENV_FILE)) fs.copyFileSync(ENV_FILE, path.join(target, '.env'));
|
|
157
|
+
if (fs.existsSync(DATA_DIR)) fs.cpSync(DATA_DIR, path.join(target, 'data'), { recursive: true, force: false, errorOnExist: false });
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
function killByPort(port) {
|
|
@@ -198,6 +216,7 @@ async function ask(question, fallback = '') {
|
|
|
198
216
|
|
|
199
217
|
async function cmdSetup() {
|
|
200
218
|
heading('Environment Setup');
|
|
219
|
+
ensureRuntimeDirs();
|
|
201
220
|
|
|
202
221
|
const current = {};
|
|
203
222
|
if (fs.existsSync(ENV_FILE)) {
|
|
@@ -217,6 +236,7 @@ async function cmdSetup() {
|
|
|
217
236
|
const openai = await ask('OpenAI API key', current.OPENAI_API_KEY || '');
|
|
218
237
|
const xai = await ask('xAI API key', current.XAI_API_KEY || '');
|
|
219
238
|
const google = await ask('Google API key', current.GOOGLE_AI_KEY || '');
|
|
239
|
+
const brave = await ask('Brave Search API key', current.BRAVE_SEARCH_API_KEY || '');
|
|
220
240
|
const ollama = await ask('Ollama URL', current.OLLAMA_URL || 'http://localhost:11434');
|
|
221
241
|
const origins = await ask('Allowed CORS origins', current.ALLOWED_ORIGINS || '');
|
|
222
242
|
|
|
@@ -228,6 +248,7 @@ async function cmdSetup() {
|
|
|
228
248
|
openai ? `OPENAI_API_KEY=${openai}` : '',
|
|
229
249
|
xai ? `XAI_API_KEY=${xai}` : '',
|
|
230
250
|
google ? `GOOGLE_AI_KEY=${google}` : '',
|
|
251
|
+
brave ? `BRAVE_SEARCH_API_KEY=${brave}` : '',
|
|
231
252
|
ollama ? `OLLAMA_URL=${ollama}` : '',
|
|
232
253
|
origins ? `ALLOWED_ORIGINS=${origins}` : ''
|
|
233
254
|
].filter(Boolean);
|
|
@@ -253,9 +274,11 @@ function installMacService() {
|
|
|
253
274
|
const nodeBin = process.execPath;
|
|
254
275
|
const content = fs
|
|
255
276
|
.readFileSync(PLIST_SRC, 'utf8')
|
|
256
|
-
.replace(
|
|
257
|
-
.replace(
|
|
258
|
-
.replace(
|
|
277
|
+
.replace(/__NODE_BIN__/g, nodeBin)
|
|
278
|
+
.replace(/__APP_DIR__/g, APP_DIR)
|
|
279
|
+
.replace(/__HOME__/g, os.homedir())
|
|
280
|
+
.replace(/__RUNTIME_HOME__/g, RUNTIME_HOME)
|
|
281
|
+
.replace(/__LOG_DIR__/g, LOG_DIR);
|
|
259
282
|
|
|
260
283
|
fs.writeFileSync(PLIST_DST, content);
|
|
261
284
|
|
|
@@ -290,8 +313,7 @@ function startFallback() {
|
|
|
290
313
|
});
|
|
291
314
|
child.unref();
|
|
292
315
|
|
|
293
|
-
fs.
|
|
294
|
-
fs.writeFileSync(path.join(APP_DIR, 'data', 'neoagent.pid'), String(child.pid));
|
|
316
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
295
317
|
logOk(`Started detached process (pid ${child.pid})`);
|
|
296
318
|
}
|
|
297
319
|
|
|
@@ -352,7 +374,7 @@ function cmdStop() {
|
|
|
352
374
|
return;
|
|
353
375
|
}
|
|
354
376
|
|
|
355
|
-
const pidPath =
|
|
377
|
+
const pidPath = PID_FILE;
|
|
356
378
|
let stopped = false;
|
|
357
379
|
if (fs.existsSync(pidPath)) {
|
|
358
380
|
const pid = Number(fs.readFileSync(pidPath, 'utf8').trim());
|
|
@@ -435,6 +457,8 @@ function cmdLogs() {
|
|
|
435
457
|
|
|
436
458
|
function cmdUpdate() {
|
|
437
459
|
heading(`Update ${APP_NAME}`);
|
|
460
|
+
migrateLegacyRuntime((msg) => logInfo(msg));
|
|
461
|
+
ensureRuntimeDirs();
|
|
438
462
|
|
|
439
463
|
if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
|
|
440
464
|
const branch = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
@@ -458,6 +482,7 @@ function cmdUpdate() {
|
|
|
458
482
|
logWarn('No git repo detected; attempting npm global update.');
|
|
459
483
|
if (commandExists('npm')) {
|
|
460
484
|
try {
|
|
485
|
+
backupRuntimeData();
|
|
461
486
|
runOrThrow('npm', ['install', '-g', 'neoagent@latest']);
|
|
462
487
|
logOk('npm global update completed');
|
|
463
488
|
} catch {
|
|
@@ -529,6 +554,8 @@ function printHelp() {
|
|
|
529
554
|
}
|
|
530
555
|
|
|
531
556
|
async function runCLI(argv) {
|
|
557
|
+
migrateLegacyRuntime((msg) => logInfo(msg));
|
|
558
|
+
ensureRuntimeDirs();
|
|
532
559
|
const command = argv[0] || 'help';
|
|
533
560
|
|
|
534
561
|
switch (command) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neoagent",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Proactive personal AI agent with no limits",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"lib",
|
|
13
|
+
"runtime",
|
|
13
14
|
"server",
|
|
14
15
|
"docs",
|
|
15
16
|
"com.neoagent.plist",
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
"start": "node server/index.js",
|
|
22
23
|
"dev": "node --watch server/index.js",
|
|
23
24
|
"manage": "node bin/neoagent.js",
|
|
25
|
+
"test": "node --test",
|
|
26
|
+
"benchmark:tokens": "node scripts/benchmark-token-cost.js",
|
|
24
27
|
"release": "npx semantic-release"
|
|
25
28
|
},
|
|
26
29
|
"repository": {
|
package/runtime/paths.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const APP_DIR = path.resolve(__dirname, '..');
|
|
6
|
+
const HOME_DIR = os.homedir();
|
|
7
|
+
const RUNTIME_HOME = path.resolve(process.env.NEOAGENT_HOME || path.join(HOME_DIR, '.neoagent'));
|
|
8
|
+
const DATA_DIR = path.resolve(process.env.NEOAGENT_DATA_DIR || path.join(RUNTIME_HOME, 'data'));
|
|
9
|
+
const AGENT_DATA_DIR = path.resolve(process.env.NEOAGENT_AGENT_DATA_DIR || path.join(RUNTIME_HOME, 'agent-data'));
|
|
10
|
+
const LOG_DIR = path.join(DATA_DIR, 'logs');
|
|
11
|
+
const ENV_FILE = path.resolve(process.env.NEOAGENT_ENV_FILE || path.join(RUNTIME_HOME, '.env'));
|
|
12
|
+
const UPDATE_STATUS_FILE = path.join(DATA_DIR, 'update-status.json');
|
|
13
|
+
const PID_FILE = path.join(DATA_DIR, 'neoagent.pid');
|
|
14
|
+
|
|
15
|
+
const LEGACY_ENV_FILE = path.join(APP_DIR, '.env');
|
|
16
|
+
const LEGACY_DATA_DIR = path.join(APP_DIR, 'data');
|
|
17
|
+
const LEGACY_AGENT_DATA_DIR = path.join(APP_DIR, 'agent-data');
|
|
18
|
+
|
|
19
|
+
function ensureRuntimeDirs() {
|
|
20
|
+
for (const dir of [RUNTIME_HOME, DATA_DIR, LOG_DIR, AGENT_DATA_DIR]) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyFileIfMissing(src, dest) {
|
|
26
|
+
if (!fs.existsSync(src) || fs.existsSync(dest)) return false;
|
|
27
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
28
|
+
fs.copyFileSync(src, dest);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyDirMerge(src, dest) {
|
|
33
|
+
if (!fs.existsSync(src)) return false;
|
|
34
|
+
if (path.resolve(src) === path.resolve(dest)) return false;
|
|
35
|
+
if (fs.existsSync(dest)) {
|
|
36
|
+
const existing = fs.readdirSync(dest);
|
|
37
|
+
if (existing.length > 0) return false;
|
|
38
|
+
}
|
|
39
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
40
|
+
fs.cpSync(src, dest, { recursive: true, force: false, errorOnExist: false });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function migrateLegacyRuntime(logger = () => {}) {
|
|
45
|
+
ensureRuntimeDirs();
|
|
46
|
+
let changed = false;
|
|
47
|
+
|
|
48
|
+
if (copyFileIfMissing(LEGACY_ENV_FILE, ENV_FILE)) {
|
|
49
|
+
try { fs.chmodSync(ENV_FILE, 0o600); } catch {}
|
|
50
|
+
logger(`migrated ${LEGACY_ENV_FILE} -> ${ENV_FILE}`);
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
if (copyDirMerge(LEGACY_DATA_DIR, DATA_DIR)) {
|
|
54
|
+
logger(`migrated ${LEGACY_DATA_DIR} -> ${DATA_DIR}`);
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
if (copyDirMerge(LEGACY_AGENT_DATA_DIR, AGENT_DATA_DIR)) {
|
|
58
|
+
logger(`migrated ${LEGACY_AGENT_DATA_DIR} -> ${AGENT_DATA_DIR}`);
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return changed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
APP_DIR,
|
|
67
|
+
HOME_DIR,
|
|
68
|
+
RUNTIME_HOME,
|
|
69
|
+
DATA_DIR,
|
|
70
|
+
AGENT_DATA_DIR,
|
|
71
|
+
LOG_DIR,
|
|
72
|
+
ENV_FILE,
|
|
73
|
+
UPDATE_STATUS_FILE,
|
|
74
|
+
PID_FILE,
|
|
75
|
+
LEGACY_ENV_FILE,
|
|
76
|
+
LEGACY_DATA_DIR,
|
|
77
|
+
LEGACY_AGENT_DATA_DIR,
|
|
78
|
+
ensureRuntimeDirs,
|
|
79
|
+
migrateLegacyRuntime
|
|
80
|
+
};
|
package/server/db/database.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
const Database = require('better-sqlite3');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const DATA_DIR = path.join(__dirname, '../..', 'data');
|
|
6
|
-
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
3
|
+
const { DATA_DIR, ensureRuntimeDirs } = require('../../runtime/paths');
|
|
4
|
+
ensureRuntimeDirs();
|
|
7
5
|
|
|
8
6
|
const DB_PATH = path.join(DATA_DIR, 'neoagent.db');
|
|
9
7
|
const db = new Database(DB_PATH);
|
|
@@ -40,6 +38,7 @@ db.exec(`
|
|
|
40
38
|
trigger_source TEXT,
|
|
41
39
|
model TEXT,
|
|
42
40
|
total_tokens INTEGER DEFAULT 0,
|
|
41
|
+
prompt_metrics TEXT,
|
|
43
42
|
error TEXT,
|
|
44
43
|
created_at TEXT DEFAULT (datetime('now')),
|
|
45
44
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
@@ -144,7 +143,10 @@ db.exec(`
|
|
|
144
143
|
model TEXT,
|
|
145
144
|
total_tokens INTEGER DEFAULT 0,
|
|
146
145
|
compaction_count INTEGER DEFAULT 0,
|
|
146
|
+
summary TEXT,
|
|
147
|
+
summary_message_count INTEGER DEFAULT 0,
|
|
147
148
|
last_compaction TEXT,
|
|
149
|
+
last_summary TEXT,
|
|
148
150
|
created_at TEXT DEFAULT (datetime('now')),
|
|
149
151
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
150
152
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
@@ -228,12 +230,84 @@ db.exec(`
|
|
|
228
230
|
CREATE INDEX IF NOT EXISTS idx_core_memory_user ON core_memory(user_id, key);
|
|
229
231
|
`);
|
|
230
232
|
|
|
233
|
+
try {
|
|
234
|
+
db.exec(`
|
|
235
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
|
|
236
|
+
content,
|
|
237
|
+
role UNINDEXED,
|
|
238
|
+
user_id UNINDEXED,
|
|
239
|
+
agent_run_id UNINDEXED,
|
|
240
|
+
tokenize = 'porter unicode61'
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
CREATE TRIGGER IF NOT EXISTS conversation_history_fts_ai AFTER INSERT ON conversation_history BEGIN
|
|
244
|
+
INSERT INTO conversation_history_fts(rowid, content, role, user_id, agent_run_id)
|
|
245
|
+
VALUES (new.id, COALESCE(new.content, ''), new.role, new.user_id, COALESCE(new.agent_run_id, ''));
|
|
246
|
+
END;
|
|
247
|
+
|
|
248
|
+
CREATE TRIGGER IF NOT EXISTS conversation_history_fts_ad AFTER DELETE ON conversation_history BEGIN
|
|
249
|
+
DELETE FROM conversation_history_fts WHERE rowid = old.id;
|
|
250
|
+
END;
|
|
251
|
+
|
|
252
|
+
CREATE TRIGGER IF NOT EXISTS conversation_history_fts_au AFTER UPDATE ON conversation_history BEGIN
|
|
253
|
+
DELETE FROM conversation_history_fts WHERE rowid = old.id;
|
|
254
|
+
INSERT INTO conversation_history_fts(rowid, content, role, user_id, agent_run_id)
|
|
255
|
+
VALUES (new.id, COALESCE(new.content, ''), new.role, new.user_id, COALESCE(new.agent_run_id, ''));
|
|
256
|
+
END;
|
|
257
|
+
|
|
258
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
259
|
+
content,
|
|
260
|
+
role UNINDEXED,
|
|
261
|
+
user_id UNINDEXED,
|
|
262
|
+
run_id UNINDEXED,
|
|
263
|
+
platform UNINDEXED,
|
|
264
|
+
platform_chat_id UNINDEXED,
|
|
265
|
+
tokenize = 'porter unicode61'
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN
|
|
269
|
+
INSERT INTO messages_fts(rowid, content, role, user_id, run_id, platform, platform_chat_id)
|
|
270
|
+
VALUES (new.id, COALESCE(new.content, ''), new.role, new.user_id, COALESCE(new.run_id, ''), COALESCE(new.platform, ''), COALESCE(new.platform_chat_id, ''));
|
|
271
|
+
END;
|
|
272
|
+
|
|
273
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN
|
|
274
|
+
DELETE FROM messages_fts WHERE rowid = old.id;
|
|
275
|
+
END;
|
|
276
|
+
|
|
277
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
|
|
278
|
+
DELETE FROM messages_fts WHERE rowid = old.id;
|
|
279
|
+
INSERT INTO messages_fts(rowid, content, role, user_id, run_id, platform, platform_chat_id)
|
|
280
|
+
VALUES (new.id, COALESCE(new.content, ''), new.role, new.user_id, COALESCE(new.run_id, ''), COALESCE(new.platform, ''), COALESCE(new.platform_chat_id, ''));
|
|
281
|
+
END;
|
|
282
|
+
`);
|
|
283
|
+
} catch {
|
|
284
|
+
// FTS5 is optional. The app still works with LIKE-based fallbacks if unavailable.
|
|
285
|
+
}
|
|
286
|
+
|
|
231
287
|
// Migrations for existing databases
|
|
232
288
|
for (const col of [
|
|
233
289
|
"ALTER TABLE scheduled_tasks ADD COLUMN run_at TEXT",
|
|
234
290
|
"ALTER TABLE scheduled_tasks ADD COLUMN one_time INTEGER DEFAULT 0",
|
|
291
|
+
"ALTER TABLE agent_runs ADD COLUMN prompt_metrics TEXT",
|
|
292
|
+
"ALTER TABLE conversations ADD COLUMN summary TEXT",
|
|
293
|
+
"ALTER TABLE conversations ADD COLUMN summary_message_count INTEGER DEFAULT 0",
|
|
294
|
+
"ALTER TABLE conversations ADD COLUMN last_summary TEXT",
|
|
235
295
|
]) {
|
|
236
296
|
try { db.exec(col); } catch { /* column already exists */ }
|
|
237
297
|
}
|
|
238
298
|
|
|
299
|
+
try {
|
|
300
|
+
db.exec(`
|
|
301
|
+
INSERT OR REPLACE INTO conversation_history_fts(rowid, content, role, user_id, agent_run_id)
|
|
302
|
+
SELECT id, COALESCE(content, ''), role, user_id, COALESCE(agent_run_id, '')
|
|
303
|
+
FROM conversation_history;
|
|
304
|
+
|
|
305
|
+
INSERT OR REPLACE INTO messages_fts(rowid, content, role, user_id, run_id, platform, platform_chat_id)
|
|
306
|
+
SELECT id, COALESCE(content, ''), role, user_id, COALESCE(run_id, ''), COALESCE(platform, ''), COALESCE(platform_chat_id, '')
|
|
307
|
+
FROM messages;
|
|
308
|
+
`);
|
|
309
|
+
} catch {
|
|
310
|
+
// Older SQLite builds without FTS5 should still let the app boot.
|
|
311
|
+
}
|
|
312
|
+
|
|
239
313
|
module.exports = db;
|
package/server/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
require('
|
|
1
|
+
const { ENV_FILE, DATA_DIR, APP_DIR, migrateLegacyRuntime, ensureRuntimeDirs } = require('../runtime/paths');
|
|
2
|
+
require('dotenv').config({ path: ENV_FILE });
|
|
3
|
+
migrateLegacyRuntime();
|
|
4
|
+
ensureRuntimeDirs();
|
|
2
5
|
|
|
3
6
|
const express = require('express');
|
|
4
7
|
const session = require('express-session');
|
|
@@ -8,7 +11,6 @@ const { Server: SocketIO } = require('socket.io');
|
|
|
8
11
|
const helmet = require('helmet');
|
|
9
12
|
const cors = require('cors');
|
|
10
13
|
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
14
|
|
|
13
15
|
const db = require('./db/database');
|
|
14
16
|
const { requireAuth, requireNoAuth } = require('./middleware/auth');
|
|
@@ -35,8 +37,6 @@ if (!process.env.SESSION_SECRET) {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const PORT = process.env.PORT || 3333;
|
|
38
|
-
const DATA_DIR = path.join(__dirname, '../data');
|
|
39
|
-
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
40
40
|
|
|
41
41
|
// ── Middleware ──
|
|
42
42
|
|
|
@@ -159,7 +159,7 @@ app.get('/api/version', requireAuth, (req, res) => {
|
|
|
159
159
|
try {
|
|
160
160
|
const { execSync } = require('child_process');
|
|
161
161
|
gitSha = execSync('git rev-parse --short HEAD', {
|
|
162
|
-
cwd:
|
|
162
|
+
cwd: APP_DIR,
|
|
163
163
|
encoding: 'utf8',
|
|
164
164
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
165
165
|
}).trim();
|