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 +21 -0
- package/README.md +205 -0
- package/bin/termsearch.js +433 -0
- package/config.example.json +31 -0
- package/frontend/dist/app.js +1051 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon.svg +8 -0
- package/frontend/dist/index.html +28 -0
- package/frontend/dist/manifest.json +40 -0
- package/frontend/dist/opensearch.xml +8 -0
- package/frontend/dist/style.css +756 -0
- package/package.json +48 -0
- package/scripts/postinstall.js +84 -0
- package/src/ai/orchestrator.js +163 -0
- package/src/ai/providers/openai-compat.js +255 -0
- package/src/ai/query.js +54 -0
- package/src/ai/summary.js +120 -0
- package/src/api/middleware.js +91 -0
- package/src/api/routes.js +461 -0
- package/src/autostart/manager.js +207 -0
- package/src/config/defaults.js +62 -0
- package/src/config/manager.js +188 -0
- package/src/fetch/document.js +297 -0
- package/src/fetch/ssrf-guard.js +40 -0
- package/src/profiler/scanner.js +212 -0
- package/src/search/cache.js +119 -0
- package/src/search/engine.js +231 -0
- package/src/search/providers/brave.js +57 -0
- package/src/search/providers/duckduckgo.js +148 -0
- package/src/search/providers/mojeek.js +56 -0
- package/src/search/providers/searxng.js +53 -0
- package/src/search/providers/wikipedia.js +70 -0
- package/src/search/ranking.js +155 -0
- package/src/server.js +68 -0
- package/src/social/scrapers.js +356 -0
- package/src/social/search.js +77 -0
- package/src/torrent/scrapers.js +125 -0
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
|
+
[](#project-status)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://termux.dev)
|
|
7
|
+
[](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
|
+
}
|