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.
Files changed (39) hide show
  1. package/.env.example +5 -0
  2. package/com.neoagent.plist +8 -6
  3. package/docs/configuration.md +9 -1
  4. package/docs/skills.md +6 -2
  5. package/lib/manager.js +37 -10
  6. package/package.json +4 -1
  7. package/runtime/paths.js +80 -0
  8. package/server/db/database.js +78 -4
  9. package/server/index.js +5 -5
  10. package/server/public/app.html +124 -49
  11. package/server/public/assets/world-office-dark.png +0 -0
  12. package/server/public/assets/world-office-light.png +0 -0
  13. package/server/public/css/app.css +575 -242
  14. package/server/public/css/styles.css +445 -121
  15. package/server/public/js/app.js +1041 -423
  16. package/server/routes/memory.js +3 -1
  17. package/server/routes/settings.js +42 -6
  18. package/server/routes/skills.js +124 -84
  19. package/server/routes/store.js +102 -1
  20. package/server/services/ai/compaction.js +15 -31
  21. package/server/services/ai/engine.js +224 -202
  22. package/server/services/ai/history.js +188 -0
  23. package/server/services/ai/learning.js +143 -0
  24. package/server/services/ai/providers/google.js +8 -1
  25. package/server/services/ai/settings.js +80 -0
  26. package/server/services/ai/systemPrompt.js +57 -98
  27. package/server/services/ai/toolResult.js +151 -0
  28. package/server/services/ai/toolRunner.js +26 -7
  29. package/server/services/ai/toolSelector.js +140 -0
  30. package/server/services/ai/tools.js +158 -5
  31. package/server/services/browser/controller.js +124 -48
  32. package/server/services/manager.js +26 -3
  33. package/server/services/mcp/client.js +1 -1
  34. package/server/services/memory/embeddings.js +80 -14
  35. package/server/services/memory/manager.js +211 -17
  36. package/server/services/messaging/telnyx.js +3 -2
  37. package/server/services/messaging/whatsapp.js +3 -2
  38. package/server/services/scheduler/cron.js +6 -1
  39. 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
@@ -7,21 +7,23 @@
7
7
 
8
8
  <key>ProgramArguments</key>
9
9
  <array>
10
- <string>/usr/local/bin/node</string>
11
- <string>/Users/neo/NeoAgent/server/index.js</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>/Users/neo/NeoAgent</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>/Users/neo</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>/Users/neo/NeoAgent/data/logs/neoagent.log</string>
42
+ <string>__LOG_DIR__/neoagent.log</string>
41
43
 
42
44
  <key>StandardErrorPath</key>
43
- <string>/Users/neo/NeoAgent/data/logs/neoagent.error.log</string>
45
+ <string>__LOG_DIR__/neoagent.error.log</string>
44
46
  </dict>
45
47
  </plist>
@@ -1,6 +1,7 @@
1
1
  # Configuration
2
2
 
3
- All settings live in `.env` at the project root. Run `neoagent setup` to regenerate interactively.
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 `agent-data/skills/` that describe capabilities the agent can use when responding to tasks. They are loaded at runtime — no restart needed after editing.
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 `agent-data/skills/`:
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
- fs.mkdirSync(LOG_DIR, { recursive: true });
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(/\/usr\/local\/bin\/node/g, nodeBin)
257
- .replace(/\/Users\/neo\/NeoAgent/g, APP_DIR)
258
- .replace(/\/Users\/neo/g, os.homedir());
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.mkdirSync(path.join(APP_DIR, 'data'), { recursive: true });
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 = path.join(APP_DIR, 'data', 'neoagent.pid');
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.0",
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": {
@@ -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
+ };
@@ -1,9 +1,7 @@
1
1
  const Database = require('better-sqlite3');
2
2
  const path = require('path');
3
- const fs = require('fs');
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('dotenv').config();
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: path.join(__dirname, '..'),
162
+ cwd: APP_DIR,
163
163
  encoding: 'utf8',
164
164
  stdio: ['ignore', 'pipe', 'ignore']
165
165
  }).trim();