termsearch 0.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide A. Guglielmi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # TermSearch - Personal Search Engine
2
+
3
+ [![Status](https://img.shields.io/badge/Status-0.3.0-blue.svg)](#project-status)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org)
6
+ [![Target](https://img.shields.io/badge/Target-Termux%20%2F%20Linux%20%2F%20macOS-green.svg)](https://termux.dev)
7
+ [![npm](https://img.shields.io/badge/npm-termsearch-red.svg)](https://www.npmjs.com/package/termsearch)
8
+
9
+ TermSearch is a privacy-first personal search engine installable with a single command on Termux, Linux, and macOS.
10
+ Zero external dependencies, no Docker, no Python. AI is optional and configured entirely from the browser.
11
+
12
+ Core capabilities:
13
+
14
+ - Zero-config search via DuckDuckGo and Wikipedia — works immediately after install
15
+ - Progressive enhancement: add Brave/Mojeek API keys, AI endpoints, or SearXNG when needed
16
+ - Social profile scanner: GitHub, Bluesky, Reddit, Twitter/X, Instagram, YouTube, LinkedIn, TikTok, Telegram, Facebook
17
+ - Torrent search: The Pirate Bay + 1337x with direct magnet extraction
18
+ - Social search: Bluesky posts/actors + GDELT news
19
+ - AI-powered 2-phase agentic summaries via any OpenAI-compatible endpoint
20
+ - Vanilla HTML/CSS/JS frontend (~280KB, no build step, no framework)
21
+ - Boot autostart: Termux:Boot, systemd --user, or launchd (macOS)
22
+
23
+ ## Project Status
24
+
25
+ - Current line: `0.3.0`
26
+ - Core is MIT — zero required API keys
27
+ - AI features are optional, configured via Settings page in browser
28
+ - Tested on: Ubuntu 24.04, Termux (Android 15/16)
29
+ - macOS: compatible (launchd autostart), untested in production
30
+
31
+ ## Quickstart
32
+
33
+ 1. Install globally
34
+
35
+ ```bash
36
+ npm install -g termsearch
37
+ ```
38
+
39
+ 2. Start
40
+
41
+ ```bash
42
+ termsearch
43
+ ```
44
+
45
+ 3. Open browser at `http://localhost:3000`
46
+
47
+ That's it. No configuration required for basic search.
48
+
49
+ ## CLI
50
+
51
+ ```bash
52
+ termsearch # start + open browser (if not running: status)
53
+ termsearch start # start server in background
54
+ termsearch start --fg # start in foreground (debug/logs live)
55
+ termsearch stop # stop background server
56
+ termsearch restart # restart
57
+ termsearch status # show PID, URL, uptime
58
+ termsearch doctor # check Node version, data dir, HTTP health
59
+ termsearch logs # last 60 lines of server log
60
+ termsearch logs -n 100 # last N lines
61
+ termsearch open # open browser
62
+ termsearch autostart enable # start at boot (Termux:Boot / systemd / launchd)
63
+ termsearch autostart disable # disable autostart
64
+ termsearch help # full command reference
65
+ ```
66
+
67
+ Options:
68
+
69
+ ```bash
70
+ --port=<port> # default: 3000
71
+ --host=<host> # default: 127.0.0.1
72
+ --data-dir=<path> # default: ~/.termsearch/
73
+ ```
74
+
75
+ ## Progressive Enhancement
76
+
77
+ | Level | Requirements | Features |
78
+ |-------|-------------|---------|
79
+ | **0** (zero-config) | None | DuckDuckGo + Wikipedia — works immediately |
80
+ | **1** (API keys) | Brave/Mojeek key via Settings | Better and more diverse results |
81
+ | **2** (AI) | Any OpenAI-compatible endpoint | Summaries, query refinement |
82
+ | **3** (power user) | Own SearXNG instance | 40+ search engines |
83
+
84
+ ## AI Configuration
85
+
86
+ Configure at **Settings → AI** in the browser. Supported endpoints:
87
+
88
+ | Provider | API Base | Model | Key |
89
+ |----------|----------|-------|-----|
90
+ | **Localhost** (Ollama) | `http://localhost:11434/v1` | `qwen3.5:4b` or any | not required |
91
+ | **Localhost** (LM Studio) | `http://localhost:1234/v1` | your loaded model | not required |
92
+ | **Chutes.ai TEE** | `https://llm.chutes.ai/v1` | `deepseek-ai/DeepSeek-V3.2-TEE` | required |
93
+ | **OpenAI** | `https://api.openai.com/v1` | `gpt-4o-mini` | required |
94
+ | **API custom** | any OpenAI-compatible URL | your model | optional |
95
+
96
+ All providers use the OpenAI-compatible `/chat/completions` format. Leave API key empty for local models.
97
+
98
+ ## Architecture
99
+
100
+ ```
101
+ ~/.termsearch/
102
+ config.json user settings (saved via browser Settings page)
103
+ cache/ search + document cache (L1 RAM + L2 disk)
104
+ termsearch.pid daemon PID
105
+ termsearch.log server log
106
+
107
+ src/
108
+ config/ config manager — load/save/defaults/env overrides
109
+ search/
110
+ providers/ DuckDuckGo, Wikipedia, Brave, Mojeek, SearXNG
111
+ engine.js fan-out, merge, rank, cache
112
+ ranking.js source diversity ranking
113
+ cache.js tiered cache (L1 Map + L2 disk JSON)
114
+ fetch/
115
+ document.js URL fetcher + HTML → readable text + site scan
116
+ ssrf-guard.js SSRF protection
117
+ ai/
118
+ orchestrator.js 2-phase agentic summary flow
119
+ summary.js prompt builder + response parser
120
+ query.js query refinement
121
+ providers/
122
+ openai-compat.js universal OpenAI-compatible client
123
+ profiler/
124
+ scanner.js social profile scanner (10 platforms)
125
+ social/
126
+ scrapers.js Twitter/Nitter, Instagram, YouTube, Facebook, LinkedIn, TikTok, Telegram
127
+ search.js Bluesky posts/actors + GDELT news
128
+ torrent/
129
+ scrapers.js TPB + 1337x scrapers + magnet extraction
130
+ autostart/
131
+ manager.js boot autostart (Termux:Boot / systemd / launchd)
132
+ api/
133
+ routes.js all API route handlers
134
+ middleware.js rate limiting, security headers
135
+ server.js Express app setup
136
+
137
+ frontend/dist/ vanilla HTML/CSS/JS SPA (~280KB, no build step)
138
+ ```
139
+
140
+ ## API
141
+
142
+ ```
143
+ GET /api/health service status + enabled providers
144
+ GET /api/openapi.json machine-readable API description
145
+ GET /api/search?q=... web search
146
+ GET /api/search-stream?q=... progressive SSE search
147
+ POST /api/fetch fetch readable content from URL
148
+ POST /api/ai-query AI query refinement
149
+ POST /api/ai-summary AI summary with SSE streaming
150
+ GET /api/profiler?q=... social profile scan (URL or @handle)
151
+ GET /api/social-search?q=... Bluesky + GDELT news
152
+ POST /api/torrent-search torrent search (TPB + 1337x)
153
+ POST /api/magnet extract magnet from torrent page URL
154
+ POST /api/scan crawl site for query-matched pages
155
+ GET /api/config current config (keys masked)
156
+ POST /api/config update and persist config
157
+ POST /api/config/test-ai test AI connection
158
+ GET /api/config/test-provider/:name test search provider
159
+ GET /api/autostart autostart status
160
+ POST /api/autostart enable/disable autostart
161
+ GET /api/stats usage stats
162
+ ```
163
+
164
+ ## Environment Variables (optional, override Settings)
165
+
166
+ ```bash
167
+ TERMSEARCH_PORT=3000
168
+ TERMSEARCH_HOST=127.0.0.1
169
+ TERMSEARCH_DATA_DIR=~/.termsearch/
170
+ TERMSEARCH_AI_API_BASE=https://api.z.ai/api/coding/paas/v4
171
+ TERMSEARCH_AI_API_KEY=
172
+ TERMSEARCH_AI_MODEL=glm-4.7
173
+ TERMSEARCH_BRAVE_API_KEY=
174
+ TERMSEARCH_MOJEEK_API_KEY=
175
+ TERMSEARCH_SEARXNG_URL=
176
+ TERMSEARCH_GITHUB_TOKEN=
177
+ TERMSEARCH_INSTAGRAM_SESSION=
178
+ ```
179
+
180
+ ## Termux
181
+
182
+ ```bash
183
+ pkg install nodejs
184
+ npm install -g termsearch
185
+ termsearch
186
+ ```
187
+
188
+ Enable autostart with Termux:Boot (install from F-Droid):
189
+
190
+ ```bash
191
+ termsearch autostart enable
192
+ ```
193
+
194
+ ## Roadmap
195
+
196
+ 1. Persistent search stats counter
197
+ 2. Engine health tracking and failure classification
198
+ 3. Frontend profile viewer panel
199
+ 4. Frontend torrent results panel with magnet copy
200
+ 5. Agentic user-proxy (`/api/user-proxy`) for custom AI loops
201
+ 6. Packaged release on npm registry
202
+
203
+ ## License
204
+
205
+ MIT — Copyright (c) 2026 Davide A. Guglielmi
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ // TermSearch CLI — personal search engine
3
+ // Usage: termsearch [command] [options]
4
+
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, openSync, readSync, unlinkSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { spawn } from 'child_process';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ // ─── Constants ────────────────────────────────────────────────────────────
15
+
16
+ const DAEMON_FLAG = '--_daemon_'; // internal flag: process is the background daemon
17
+ const PKG_PATH = path.join(__dirname, '../package.json');
18
+ const SERVER_PATH = path.join(__dirname, '../src/server.js');
19
+
20
+ const RESET = '\x1b[0m';
21
+ const GREEN = '\x1b[32m';
22
+ const YELLOW = '\x1b[33m';
23
+ const RED = '\x1b[31m';
24
+ const CYAN = '\x1b[36m';
25
+ const BOLD = '\x1b[1m';
26
+ const DIM = '\x1b[2m';
27
+
28
+ function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
29
+ function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
30
+ function err(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
31
+ function info(msg) { console.log(` ${CYAN}→${RESET} ${msg}`); }
32
+
33
+ // ─── Package version ──────────────────────────────────────────────────────
34
+
35
+ let VERSION = '0.3.0';
36
+ try { VERSION = JSON.parse(readFileSync(PKG_PATH, 'utf8')).version || VERSION; } catch { /* ignore */ }
37
+
38
+ // ─── Data dir + paths ─────────────────────────────────────────────────────
39
+
40
+ function getDataDir() {
41
+ return process.env.TERMSEARCH_DATA_DIR || path.join(os.homedir(), '.termsearch');
42
+ }
43
+
44
+ function getPaths() {
45
+ const d = getDataDir();
46
+ return {
47
+ dir: d,
48
+ pid: path.join(d, 'termsearch.pid'),
49
+ log: path.join(d, 'termsearch.log'),
50
+ };
51
+ }
52
+
53
+ // ─── PID helpers ──────────────────────────────────────────────────────────
54
+
55
+ function readPid(pidPath) {
56
+ try { return parseInt(readFileSync(pidPath, 'utf8').trim(), 10); } catch { return null; }
57
+ }
58
+
59
+ function isRunning(pid) {
60
+ if (!pid) return false;
61
+ try { process.kill(pid, 0); return true; } catch { return false; }
62
+ }
63
+
64
+ function getStatus() {
65
+ const { pid: pidPath } = getPaths();
66
+ const pid = readPid(pidPath);
67
+ const running = isRunning(pid);
68
+ return { pid: running ? pid : null, running };
69
+ }
70
+
71
+ // ─── Port / URL ───────────────────────────────────────────────────────────
72
+
73
+ function getPort() { return process.env.TERMSEARCH_PORT || '3000'; }
74
+ function getHost() { return process.env.TERMSEARCH_HOST || '127.0.0.1'; }
75
+ function getUrl() { return `http://${getHost()}:${getPort()}`; }
76
+
77
+ // ─── CLI argument parsing ─────────────────────────────────────────────────
78
+
79
+ function parseArgs(argv) {
80
+ const args = argv.slice(2);
81
+ const flags = {};
82
+ const cmds = [];
83
+ for (const a of args) {
84
+ if (a.startsWith('--')) {
85
+ const [k, v] = a.slice(2).split('=');
86
+ flags[k] = v ?? true;
87
+ } else {
88
+ cmds.push(a);
89
+ }
90
+ }
91
+ return { cmd: cmds[0] || null, sub: cmds[1] || null, flags };
92
+ }
93
+
94
+ // ─── DAEMON MODE ─────────────────────────────────────────────────────────
95
+ // When spawned with DAEMON_FLAG, just run the server (stdout → log file)
96
+
97
+ if (process.argv.includes(DAEMON_FLAG)) {
98
+ // Apply env vars from flags
99
+ for (const arg of process.argv.slice(2)) {
100
+ if (arg === DAEMON_FLAG) continue;
101
+ const [k, v] = arg.split('=');
102
+ if (k === '--port' && v) process.env.TERMSEARCH_PORT = v;
103
+ if (k === '--host' && v) process.env.TERMSEARCH_HOST = v;
104
+ if (k === '--data-dir' && v) process.env.TERMSEARCH_DATA_DIR = v;
105
+ }
106
+
107
+ const { port, host } = await import(SERVER_PATH);
108
+ const dataDir = getDataDir();
109
+ const aiBase = process.env.TERMSEARCH_AI_API_BASE || '';
110
+ const aiModel = process.env.TERMSEARCH_AI_MODEL || '';
111
+
112
+ console.log(`TermSearch v${VERSION} started`);
113
+ console.log(`Data: ${dataDir}`);
114
+ console.log(`URL: http://${host}:${port}`);
115
+ if (aiBase && aiModel) console.log(`AI: ${aiModel} @ ${aiBase}`);
116
+ process.exit ?? undefined; // keep running (server keeps the loop alive)
117
+ process.exit; // unreachable
118
+ } else {
119
+ // ─── USER-FACING CLI ───────────────────────────────────────────────────
120
+ await runCli();
121
+ }
122
+
123
+ async function runCli() {
124
+ const { cmd, sub, flags } = parseArgs(process.argv);
125
+
126
+ // Apply flags to env
127
+ if (flags.port) process.env.TERMSEARCH_PORT = flags.port;
128
+ if (flags.host) process.env.TERMSEARCH_HOST = flags.host;
129
+ if (flags['data-dir']) process.env.TERMSEARCH_DATA_DIR = flags['data-dir'];
130
+
131
+ if (flags.help || flags.h || cmd === 'help') return printHelp();
132
+ if (flags.version || flags.v || cmd === 'version') {
133
+ console.log(`termsearch v${VERSION}`);
134
+ return;
135
+ }
136
+
137
+ switch (cmd) {
138
+ case 'start': return cmdStart(flags);
139
+ case 'stop': return cmdStop();
140
+ case 'restart': return cmdRestart(flags);
141
+ case 'status': return cmdStatus();
142
+ case 'open': return cmdOpen();
143
+ case 'logs': return cmdLogs(flags);
144
+ case 'doctor': return cmdDoctor();
145
+ case 'autostart':return cmdAutostart(sub);
146
+ case null: return cmdDefault(flags);
147
+ default:
148
+ err(`Unknown command: ${cmd}`);
149
+ console.log(` Run ${BOLD}termsearch help${RESET} for usage`);
150
+ process.exit(1);
151
+ }
152
+ }
153
+
154
+ // ─── Commands ─────────────────────────────────────────────────────────────
155
+
156
+ async function cmdStart(flags) {
157
+ const { running, pid } = getStatus();
158
+ if (running) {
159
+ warn(`Already running (PID ${pid} ${getUrl()})`);
160
+ return;
161
+ }
162
+
163
+ if (flags.fg || flags.foreground) {
164
+ // Foreground mode — import server directly
165
+ console.log('');
166
+ const { port, host } = await import(SERVER_PATH);
167
+ const dataDir = getDataDir();
168
+ const aiBase = process.env.TERMSEARCH_AI_API_BASE || '';
169
+ const aiModel = process.env.TERMSEARCH_AI_MODEL || '';
170
+ console.log(`
171
+ ${BOLD}TermSearch v${VERSION}${RESET}
172
+ Personal search engine — privacy-first, local-first
173
+
174
+ ${GREEN}✓${RESET} Data: ${dataDir}
175
+ ${GREEN}✓${RESET} Search: DuckDuckGo + Wikipedia${
176
+ aiBase && aiModel ? `\n ${GREEN}✓${RESET} AI: ${aiModel}` : `\n ${DIM}○ AI: not configured (Settings → AI)${RESET}`
177
+ }
178
+
179
+ ${CYAN}→${RESET} ${BOLD}${getUrl()}${RESET}
180
+
181
+ Press Ctrl+C to stop
182
+ `);
183
+ return;
184
+ }
185
+
186
+ // Background daemon
187
+ const paths = getPaths();
188
+ mkdirSync(paths.dir, { recursive: true });
189
+
190
+ const logFd = openSync(paths.log, 'a');
191
+ const passArgs = [];
192
+ if (process.env.TERMSEARCH_PORT) passArgs.push(`--port=${process.env.TERMSEARCH_PORT}`);
193
+ if (process.env.TERMSEARCH_HOST) passArgs.push(`--host=${process.env.TERMSEARCH_HOST}`);
194
+ if (process.env.TERMSEARCH_DATA_DIR) passArgs.push(`--data-dir=${process.env.TERMSEARCH_DATA_DIR}`);
195
+ passArgs.push(DAEMON_FLAG);
196
+
197
+ const child = spawn(process.execPath, [__filename, ...passArgs], {
198
+ detached: true,
199
+ stdio: ['ignore', logFd, logFd],
200
+ env: { ...process.env },
201
+ });
202
+ child.unref();
203
+
204
+ // Wait briefly to confirm it started
205
+ await sleep(600);
206
+ if (!isRunning(child.pid)) {
207
+ err('Failed to start — check logs: termsearch logs');
208
+ process.exit(1);
209
+ }
210
+
211
+ writeFileSync(paths.pid, String(child.pid));
212
+ ok(`Started (PID ${child.pid})`);
213
+ info(`${BOLD}${getUrl()}${RESET}`);
214
+ info(`Logs: ${paths.log}`);
215
+ }
216
+
217
+ async function cmdStop() {
218
+ const paths = getPaths();
219
+ const { running, pid } = getStatus();
220
+ if (!running) {
221
+ warn('Not running');
222
+ return;
223
+ }
224
+ try {
225
+ process.kill(pid, 'SIGTERM');
226
+ await sleep(800);
227
+ if (isRunning(pid)) {
228
+ process.kill(pid, 'SIGKILL');
229
+ await sleep(300);
230
+ }
231
+ ok(`Stopped (was PID ${pid})`);
232
+ try { unlinkSync(paths.pid); } catch { /* ignore */ }
233
+ } catch (e) {
234
+ err(`Stop failed: ${e.message}`);
235
+ process.exit(1);
236
+ }
237
+ }
238
+
239
+ async function cmdRestart(flags) {
240
+ await cmdStop();
241
+ await sleep(400);
242
+ await cmdStart(flags);
243
+ }
244
+
245
+ function cmdStatus() {
246
+ const { running, pid } = getStatus();
247
+ const paths = getPaths();
248
+ console.log('');
249
+ if (running) {
250
+ ok(`${BOLD}Running${RESET} (PID ${pid})`);
251
+ info(`${getUrl()}`);
252
+ info(`Data: ${paths.dir}`);
253
+ info(`Logs: ${paths.log}`);
254
+ } else {
255
+ warn('Stopped');
256
+ info(`Run ${BOLD}termsearch start${RESET} to start`);
257
+ }
258
+ console.log('');
259
+ }
260
+
261
+ function cmdOpen() {
262
+ const url = getUrl();
263
+ const isTermux = process.env.PREFIX?.includes('com.termux');
264
+ const opener = isTermux
265
+ ? ['termux-open-url', [url]]
266
+ : process.platform === 'darwin'
267
+ ? ['open', [url]]
268
+ : ['xdg-open', [url]];
269
+ try {
270
+ spawn(opener[0], opener[1], { stdio: 'ignore', detached: true }).unref();
271
+ info(`Opening ${url}`);
272
+ } catch {
273
+ info(`Open manually: ${BOLD}${url}${RESET}`);
274
+ }
275
+ }
276
+
277
+ function cmdLogs(flags) {
278
+ const { log: logPath } = getPaths();
279
+ if (!existsSync(logPath)) {
280
+ warn('No log file yet — start termsearch first');
281
+ return;
282
+ }
283
+ const lines = flags.n ? parseInt(flags.n, 10) : 60;
284
+ const content = readFileSync(logPath, 'utf8');
285
+ const tail = content.split('\n').slice(-lines).join('\n');
286
+ console.log(tail);
287
+ }
288
+
289
+ async function cmdDoctor() {
290
+ const paths = getPaths();
291
+ const { running, pid } = getStatus();
292
+ const port = getPort();
293
+ let allOk = true;
294
+
295
+ console.log('');
296
+ console.log(`${BOLD} TermSearch v${VERSION} — doctor${RESET}`);
297
+ console.log('');
298
+
299
+ // Node.js version
300
+ const [major] = process.versions.node.split('.').map(Number);
301
+ if (major >= 18) { ok(`Node.js ${process.versions.node}`); }
302
+ else { err(`Node.js ${process.versions.node} — requires ≥ 18`); allOk = false; }
303
+
304
+ // Platform
305
+ const isTermux = process.env.PREFIX?.includes('com.termux') || existsSync('/data/data/com.termux');
306
+ const platform = isTermux ? 'Termux' : process.platform === 'darwin' ? 'macOS' : process.platform === 'linux' ? 'Linux' : process.platform;
307
+ ok(`Platform: ${platform}`);
308
+
309
+ // Data dir
310
+ try {
311
+ const { mkdirSync, accessSync, constants } = await import('fs');
312
+ mkdirSync(paths.dir, { recursive: true });
313
+ accessSync(paths.dir, constants.W_OK);
314
+ ok(`Data dir: ${paths.dir}`);
315
+ } catch { err(`Data dir not writable: ${paths.dir}`); allOk = false; }
316
+
317
+ // Server status
318
+ if (running) { ok(`Server: running (PID ${pid})`); }
319
+ else { warn('Server: not running'); }
320
+
321
+ // HTTP health check (only if running)
322
+ if (running) {
323
+ try {
324
+ const ac = new AbortController();
325
+ setTimeout(() => ac.abort(), 3000);
326
+ const r = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: ac.signal });
327
+ if (r.ok) {
328
+ const h = await r.json();
329
+ ok(`HTTP: ${getUrl()} — providers: ${(h.providers || []).join(', ')}`);
330
+ if (h.ai_enabled) ok(`AI: configured (${h.ai_model})`);
331
+ else warn('AI: not configured (optional — Settings → AI)');
332
+ } else { err(`HTTP: ${r.status} from /api/health`); allOk = false; }
333
+ } catch (e) { err(`HTTP: cannot reach ${getUrl()} — ${e.message}`); allOk = false; }
334
+ }
335
+
336
+ console.log('');
337
+ if (allOk) { ok(`${GREEN}All checks passed${RESET}`); }
338
+ else { warn('Some checks failed — see above'); }
339
+ console.log('');
340
+ }
341
+
342
+ async function cmdAutostart(sub) {
343
+ const { getStatus: autostartStatus, setEnabled } = await import('../src/autostart/manager.js');
344
+ if (sub === 'enable' || sub === 'disable') {
345
+ try {
346
+ const status = setEnabled(sub === 'enable');
347
+ if (status.enabled) {
348
+ ok(`Autostart enabled (${status.method})`);
349
+ info(`Config: ${status.config_path}`);
350
+ } else {
351
+ ok(`Autostart disabled`);
352
+ }
353
+ if (status.note) warn(status.note);
354
+ } catch (e) {
355
+ err(`Failed: ${e.message}`);
356
+ }
357
+ return;
358
+ }
359
+ // Show status
360
+ const status = autostartStatus();
361
+ console.log('');
362
+ info(`Platform : ${status.platform}`);
363
+ info(`Method : ${status.method || 'N/A'}`);
364
+ info(`Status : ${status.enabled ? `${GREEN}enabled${RESET}` : `${DIM}disabled${RESET}`}`);
365
+ if (status.config_path) info(`Config : ${status.config_path}`);
366
+ if (status.note) warn(status.note);
367
+ if (!status.enabled) info(`Enable : ${BOLD}termsearch autostart enable${RESET}`);
368
+ console.log('');
369
+ }
370
+
371
+ async function cmdDefault(flags) {
372
+ const { running, pid } = getStatus();
373
+ if (running) {
374
+ cmdStatus();
375
+ } else {
376
+ await cmdStart(flags);
377
+ if (getStatus().running) {
378
+ await sleep(300);
379
+ cmdOpen();
380
+ }
381
+ }
382
+ }
383
+
384
+ // ─── Help ─────────────────────────────────────────────────────────────────
385
+
386
+ function printHelp() {
387
+ console.log(`
388
+ ${BOLD}TermSearch v${VERSION}${RESET}
389
+ Personal search engine — privacy-first, local-first
390
+
391
+ ${BOLD}Usage:${RESET}
392
+ ${CYAN}termsearch${RESET} Start (if stopped) or show status
393
+ ${CYAN}termsearch${RESET} <command> [options]
394
+
395
+ ${BOLD}Commands:${RESET}
396
+ ${GREEN}start${RESET} Start server in background
397
+ ${GREEN}start --fg${RESET} Start in foreground (shows logs live)
398
+ ${GREEN}stop${RESET} Stop background server
399
+ ${GREEN}restart${RESET} Restart server
400
+ ${GREEN}status${RESET} Show running status + URL
401
+ ${GREEN}open${RESET} Open browser to http://localhost:3000
402
+ ${GREEN}logs${RESET} [-n <lines>] Show server log (default: last 60 lines)
403
+ ${GREEN}doctor${RESET} Check Node.js, data dir, server health
404
+ ${GREEN}autostart${RESET} Show autostart (boot) status
405
+ ${GREEN}autostart enable${RESET} Enable autostart at boot
406
+ ${GREEN}autostart disable${RESET} Disable autostart at boot
407
+ ${GREEN}version${RESET} Print version
408
+ ${GREEN}help${RESET} Show this help
409
+
410
+ ${BOLD}Options:${RESET}
411
+ --port=<port> Port (default: 3000)
412
+ --host=<host> Host (default: 127.0.0.1)
413
+ --data-dir=<path> Data dir (default: ~/.termsearch/)
414
+
415
+ ${BOLD}Examples:${RESET}
416
+ termsearch # start + open browser
417
+ termsearch start # start in background
418
+ termsearch start --fg # start in foreground
419
+ termsearch stop # stop
420
+ termsearch status # check if running
421
+ termsearch logs -n 100 # last 100 log lines
422
+ termsearch autostart enable # start at boot
423
+ termsearch --port=8080 start # custom port
424
+
425
+ ${BOLD}Data:${RESET} ~/.termsearch/config.json (edit via Settings in browser)
426
+ ${BOLD}Logs:${RESET} ~/.termsearch/termsearch.log
427
+ ${BOLD}URL:${RESET} http://localhost:3000
428
+ `);
429
+ }
430
+
431
+ // ─── Utilities ────────────────────────────────────────────────────────────
432
+
433
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
@@ -0,0 +1,31 @@
1
+ {
2
+ "port": 3000,
3
+ "host": "127.0.0.1",
4
+ "search": {
5
+ "providers": ["duckduckgo", "wikipedia"]
6
+ },
7
+ "ai": {
8
+ "enabled": false,
9
+ "api_base": "http://localhost:11434/v1",
10
+ "api_key": "",
11
+ "model": "qwen3.5:4b",
12
+ "_providers": {
13
+ "localhost_ollama": { "api_base": "http://localhost:11434/v1", "model": "qwen3.5:4b" },
14
+ "localhost_lmstudio": { "api_base": "http://localhost:1234/v1", "model": "your-model" },
15
+ "chutes_tee": { "api_base": "https://llm.chutes.ai/v1", "model": "deepseek-ai/DeepSeek-V3.2-TEE" },
16
+ "openai": { "api_base": "https://api.openai.com/v1", "model": "gpt-4o-mini" }
17
+ }
18
+ },
19
+ "brave": {
20
+ "enabled": false,
21
+ "api_key": ""
22
+ },
23
+ "mojeek": {
24
+ "enabled": false,
25
+ "api_key": ""
26
+ },
27
+ "searxng": {
28
+ "enabled": false,
29
+ "url": ""
30
+ }
31
+ }