termsearch 0.3.6 → 0.3.8
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/README.md +111 -145
- package/bin/termsearch.js +1 -1
- package/frontend/dist/app.js +65 -14
- package/frontend/dist/style.css +70 -5
- package/package.json +1 -1
- package/src/api/routes.js +14 -1
- package/src/server.js +14 -1
package/README.md
CHANGED
|
@@ -1,187 +1,167 @@
|
|
|
1
|
-
# TermSearch
|
|
1
|
+
# TermSearch
|
|
2
2
|
|
|
3
|
-
[](#)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
|
-
[](#)
|
|
7
7
|
[](https://www.npmjs.com/package/termsearch)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Zero external dependencies, no Docker, no Python. AI is optional and configured entirely from the browser.
|
|
9
|
+
**Personal search engine.** One command install, zero config, privacy-first.
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
No Docker, no Python, no API keys required. AI is optional. Everything runs from a single `npm install`.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
- Built-in GitHub Search API fallback (`github-api`) and selectable engine mix from UI
|
|
16
|
-
- Progressive enhancement: add Brave/Mojeek API keys, AI endpoints, or SearXNG when needed
|
|
17
|
-
- Search history persistence (opt-in toggle in Settings)
|
|
18
|
-
- Social profile scanner: GitHub, Bluesky, Reddit, Twitter/X, Instagram, YouTube, LinkedIn, TikTok, Telegram, Facebook
|
|
19
|
-
- Torrent search: The Pirate Bay + 1337x with direct magnet extraction
|
|
20
|
-
- Social search: Bluesky posts/actors + GDELT news
|
|
21
|
-
- AI-powered 2-phase agentic summaries via any OpenAI-compatible endpoint
|
|
22
|
-
- Vanilla HTML/CSS/JS frontend (~280KB, no build step, no framework)
|
|
23
|
-
- Boot autostart: Termux:Boot, systemd --user, or launchd (macOS)
|
|
13
|
+
## Install & Run
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g termsearch
|
|
17
|
+
termsearch
|
|
18
|
+
```
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
- Core is MIT — zero required API keys
|
|
29
|
-
- AI features are optional, configured via Settings page in browser
|
|
30
|
-
- Tested on: Ubuntu 24.04, Termux (Android 15/16)
|
|
31
|
-
- macOS: compatible (launchd autostart), untested in production
|
|
20
|
+
Opens `http://localhost:3000`. That's it.
|
|
32
21
|
|
|
33
|
-
##
|
|
22
|
+
## What You Get
|
|
34
23
|
|
|
35
|
-
|
|
24
|
+
**Search** — DuckDuckGo + Wikipedia out of the box. Add Brave, Mojeek, Yandex, Marginalia, Ahmia, or your own SearXNG for more coverage. Engine picker lets you mix and match per-search.
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
npm install -g termsearch
|
|
39
|
-
```
|
|
26
|
+
**AI Summaries** — Connect any OpenAI-compatible endpoint (Ollama, LM Studio, llama.cpp, Chutes.ai, Anthropic, OpenAI). 2-phase agentic flow: AI picks sources, reads pages, synthesizes an answer. Session memory carries context across queries.
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
**Social Profiler** — Paste a GitHub/Bluesky/Reddit/Twitter URL or @handle, get a profile card with stats, top repos, similar accounts.
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
termsearch
|
|
45
|
-
```
|
|
30
|
+
**Torrent Search** — The Pirate Bay + 1337x with magnet links, seeders, file sizes.
|
|
46
31
|
|
|
47
|
-
|
|
32
|
+
**Social & News** — Bluesky posts + GDELT articles inline.
|
|
48
33
|
|
|
49
|
-
|
|
34
|
+
## Progressive Enhancement
|
|
35
|
+
|
|
36
|
+
| Level | Config | What works |
|
|
37
|
+
|-------|--------|------------|
|
|
38
|
+
| **0** | None | DuckDuckGo + Wikipedia + Yandex + Marginalia + Ahmia |
|
|
39
|
+
| **1** | API keys (Settings) | + Brave, Mojeek |
|
|
40
|
+
| **2** | AI endpoint (Settings) | + AI summaries, query refinement, session memory |
|
|
41
|
+
| **3** | SearXNG URL | + 40 engines via your SearXNG instance |
|
|
50
42
|
|
|
51
43
|
## CLI
|
|
52
44
|
|
|
53
|
-
```
|
|
54
|
-
termsearch
|
|
55
|
-
termsearch start
|
|
56
|
-
termsearch start --fg
|
|
57
|
-
termsearch stop
|
|
58
|
-
termsearch
|
|
59
|
-
termsearch
|
|
60
|
-
termsearch
|
|
61
|
-
termsearch
|
|
62
|
-
termsearch
|
|
63
|
-
termsearch
|
|
64
|
-
termsearch autostart enable # start at boot (Termux:Boot / systemd / launchd)
|
|
65
|
-
termsearch autostart disable # disable autostart
|
|
66
|
-
termsearch help # full command reference
|
|
45
|
+
```
|
|
46
|
+
termsearch start + open browser
|
|
47
|
+
termsearch start background daemon
|
|
48
|
+
termsearch start --fg foreground (live logs)
|
|
49
|
+
termsearch stop / restart manage daemon
|
|
50
|
+
termsearch status version, PID, URL, update check
|
|
51
|
+
termsearch doctor full health check + npm update check
|
|
52
|
+
termsearch logs [-n 100] server log tail
|
|
53
|
+
termsearch open open browser
|
|
54
|
+
termsearch autostart enable boot start (Termux:Boot / systemd / launchd)
|
|
55
|
+
termsearch help full reference
|
|
67
56
|
```
|
|
68
57
|
|
|
69
|
-
Options:
|
|
58
|
+
Options: `--port=3000` `--host=127.0.0.1` `--data-dir=~/.termsearch/`
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
--port=<port> # default: 3000
|
|
73
|
-
--host=<host> # default: 127.0.0.1
|
|
74
|
-
--data-dir=<path> # default: ~/.termsearch/
|
|
75
|
-
```
|
|
60
|
+
## AI Presets
|
|
76
61
|
|
|
77
|
-
|
|
62
|
+
Configure in **Settings > AI** from the browser. Presets auto-fill endpoint and model:
|
|
63
|
+
|
|
64
|
+
| Preset | Endpoint | Key | Default Model |
|
|
65
|
+
|--------|----------|-----|---------------|
|
|
66
|
+
| Ollama | `localhost:11434/v1` | no | `qwen3.5:4b` |
|
|
67
|
+
| LM Studio | `localhost:1234/v1` | no | — |
|
|
68
|
+
| llama.cpp | `localhost:8080/v1` | no | — |
|
|
69
|
+
| Chutes.ai TEE | `llm.chutes.ai/v1` | yes | `DeepSeek-V3.2-TEE` |
|
|
70
|
+
| Anthropic | `api.anthropic.com/v1` | yes | `claude-3-5-haiku-latest` |
|
|
71
|
+
| OpenAI | `api.openai.com/v1` | yes | `gpt-4o-mini` |
|
|
72
|
+
| Custom | any OpenAI-compatible | optional | — |
|
|
73
|
+
|
|
74
|
+
Load Models button auto-discovers available models from the endpoint.
|
|
75
|
+
|
|
76
|
+
## Search Engines
|
|
77
|
+
|
|
78
|
+
**Zero-config** (no API key): DuckDuckGo, Wikipedia, GitHub, Yandex, Ahmia, Marginalia
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|-------|-------------|---------|
|
|
81
|
-
| **0** (zero-config) | None | DuckDuckGo + Wikipedia — works immediately |
|
|
82
|
-
| **1** (API keys) | Brave/Mojeek key via Settings | Better and more diverse results |
|
|
83
|
-
| **2** (AI) | Any OpenAI-compatible endpoint | Summaries, query refinement |
|
|
84
|
-
| **3** (power user) | Own SearXNG instance | 40+ search engines |
|
|
80
|
+
**API key** (toggle in Settings): Brave Search, Mojeek
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
**Self-hosted**: SearXNG (proxy to 40+ engines)
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
**Selectable per-search**: Engine picker icon in the header lets you toggle individual engines, use presets (All / Balanced / GitHub Focus), or pick from groups (Web Core, Uncensored, Code & Dev, Media, Research, Federated, Torrent).
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|----------|----------|-------|-----|
|
|
92
|
-
| **Localhost** (Ollama) | `http://localhost:11434/v1` | `qwen3.5:4b` or any | not required |
|
|
93
|
-
| **Localhost** (LM Studio) | `http://localhost:1234/v1` | your loaded model | not required |
|
|
94
|
-
| **Localhost** (llama.cpp) | `http://localhost:8080/v1` | your loaded model | not required |
|
|
95
|
-
| **Chutes.ai TEE** | `https://llm.chutes.ai/v1` | `deepseek-ai/DeepSeek-V3.2-TEE` | required |
|
|
96
|
-
| **Anthropic** | `https://api.anthropic.com/v1` | `claude-3-5-haiku-latest` | required |
|
|
97
|
-
| **OpenAI** | `https://api.openai.com/v1` | `gpt-4o-mini` | required |
|
|
98
|
-
| **OpenRouter** | `https://openrouter.ai/api/v1` | any listed model | required |
|
|
99
|
-
| **API custom** | any OpenAI-compatible URL | your model | optional |
|
|
86
|
+
## Frontend
|
|
100
87
|
|
|
101
|
-
|
|
88
|
+
Vanilla HTML/CSS/JS — ~350KB total, no build step, no framework, no WASM.
|
|
89
|
+
|
|
90
|
+
- Dark/light theme toggle
|
|
91
|
+
- Mobile-first responsive layout with bottom bar
|
|
92
|
+
- PWA manifest + OpenSearch integration
|
|
93
|
+
- AI panel with progress bar, steps, source pills, session memory, retry
|
|
94
|
+
- Engine picker as compact icon with dropdown
|
|
102
95
|
|
|
103
96
|
## Architecture
|
|
104
97
|
|
|
105
98
|
```
|
|
106
99
|
~/.termsearch/
|
|
107
|
-
config.json
|
|
108
|
-
cache/
|
|
109
|
-
termsearch.pid
|
|
110
|
-
termsearch.log
|
|
100
|
+
config.json settings (saved via browser UI)
|
|
101
|
+
cache/ L1 RAM + L2 disk cache
|
|
102
|
+
termsearch.pid daemon PID
|
|
103
|
+
termsearch.log server log
|
|
111
104
|
|
|
112
105
|
src/
|
|
113
|
-
|
|
106
|
+
server.js Express app (~70 lines)
|
|
107
|
+
config/ manager + defaults + env overrides
|
|
114
108
|
search/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
ranking.js
|
|
118
|
-
cache.js
|
|
119
|
-
fetch/
|
|
120
|
-
document.js URL fetcher + HTML → readable text + site scan
|
|
121
|
-
ssrf-guard.js SSRF protection
|
|
109
|
+
engine.js fan-out, merge, rank, health tracking
|
|
110
|
+
providers/ ddg, wikipedia, brave, mojeek, searxng, github, yandex, ahmia, marginalia
|
|
111
|
+
ranking.js source diversity ranking
|
|
112
|
+
cache.js tiered L1+L2 cache
|
|
122
113
|
ai/
|
|
123
|
-
orchestrator.js
|
|
124
|
-
summary.js
|
|
125
|
-
query.js
|
|
126
|
-
providers/
|
|
127
|
-
|
|
128
|
-
profiler/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
autostart/
|
|
136
|
-
manager.js boot autostart (Termux:Boot / systemd / launchd)
|
|
137
|
-
api/
|
|
138
|
-
routes.js all API route handlers
|
|
139
|
-
middleware.js rate limiting, security headers
|
|
140
|
-
server.js Express app setup
|
|
141
|
-
|
|
142
|
-
frontend/dist/ vanilla HTML/CSS/JS SPA (~280KB, no build step)
|
|
114
|
+
orchestrator.js 2-phase agentic summary
|
|
115
|
+
summary.js prompt builder + parser
|
|
116
|
+
query.js query refinement
|
|
117
|
+
providers/ openai-compat universal client
|
|
118
|
+
fetch/ document fetcher + SSRF guard
|
|
119
|
+
profiler/ social profile scanner (10 platforms)
|
|
120
|
+
social/ Bluesky + GDELT + scrapers
|
|
121
|
+
torrent/ TPB + 1337x + magnet extraction
|
|
122
|
+
autostart/ Termux:Boot / systemd / launchd
|
|
123
|
+
api/ routes + middleware
|
|
124
|
+
|
|
125
|
+
frontend/dist/ vanilla SPA
|
|
143
126
|
```
|
|
144
127
|
|
|
145
128
|
## API
|
|
146
129
|
|
|
147
130
|
```
|
|
148
|
-
GET /api/health
|
|
149
|
-
GET /api/
|
|
150
|
-
GET /api/search?q=...
|
|
151
|
-
|
|
152
|
-
POST /api/
|
|
153
|
-
POST /api/
|
|
154
|
-
|
|
155
|
-
GET /api/
|
|
156
|
-
|
|
157
|
-
POST /api/
|
|
158
|
-
POST /api/
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
POST /api/config
|
|
162
|
-
POST /api/config/
|
|
163
|
-
GET /api/config/test-provider/:
|
|
164
|
-
GET /api/autostart
|
|
165
|
-
POST /api/autostart
|
|
166
|
-
GET /api/
|
|
131
|
+
GET /api/health status + providers
|
|
132
|
+
GET /api/search?q=... search (JSON)
|
|
133
|
+
GET /api/search-stream?q=... progressive search (SSE)
|
|
134
|
+
POST /api/ai-summary AI summary (SSE streaming)
|
|
135
|
+
POST /api/ai-query AI query refinement
|
|
136
|
+
POST /api/fetch fetch readable content
|
|
137
|
+
GET /api/profiler?q=... social profile scan
|
|
138
|
+
GET /api/social-search?q=... Bluesky + GDELT
|
|
139
|
+
POST /api/torrent-search torrent scrape
|
|
140
|
+
POST /api/magnet magnet extraction
|
|
141
|
+
POST /api/scan site crawl
|
|
142
|
+
GET /api/config config (keys masked)
|
|
143
|
+
POST /api/config update config
|
|
144
|
+
POST /api/config/test-ai test AI connection
|
|
145
|
+
POST /api/config/models list provider models
|
|
146
|
+
GET /api/config/test-provider/:id test search provider
|
|
147
|
+
GET /api/autostart autostart status
|
|
148
|
+
POST /api/autostart toggle autostart
|
|
149
|
+
GET /api/openapi.json OpenAPI spec
|
|
167
150
|
```
|
|
168
151
|
|
|
169
|
-
##
|
|
152
|
+
## Env Vars (optional)
|
|
170
153
|
|
|
171
154
|
```bash
|
|
172
155
|
TERMSEARCH_PORT=3000
|
|
173
156
|
TERMSEARCH_HOST=127.0.0.1
|
|
174
157
|
TERMSEARCH_DATA_DIR=~/.termsearch/
|
|
175
|
-
TERMSEARCH_AI_API_BASE=
|
|
158
|
+
TERMSEARCH_AI_API_BASE=http://localhost:11434/v1
|
|
176
159
|
TERMSEARCH_AI_API_KEY=
|
|
177
|
-
TERMSEARCH_AI_MODEL=
|
|
160
|
+
TERMSEARCH_AI_MODEL=qwen3.5:4b
|
|
178
161
|
TERMSEARCH_BRAVE_API_KEY=
|
|
179
162
|
TERMSEARCH_MOJEEK_API_KEY=
|
|
180
163
|
TERMSEARCH_MARGINALIA_API_KEY=public
|
|
181
|
-
TERMSEARCH_MARGINALIA_API_BASE=https://api2.marginalia-search.com
|
|
182
164
|
TERMSEARCH_SEARXNG_URL=
|
|
183
|
-
TERMSEARCH_GITHUB_TOKEN=
|
|
184
|
-
TERMSEARCH_INSTAGRAM_SESSION=
|
|
185
165
|
```
|
|
186
166
|
|
|
187
167
|
## Termux
|
|
@@ -190,23 +170,9 @@ TERMSEARCH_INSTAGRAM_SESSION=
|
|
|
190
170
|
pkg install nodejs
|
|
191
171
|
npm install -g termsearch
|
|
192
172
|
termsearch
|
|
173
|
+
termsearch autostart enable # optional: start on boot
|
|
193
174
|
```
|
|
194
175
|
|
|
195
|
-
Enable autostart with Termux:Boot (install from F-Droid):
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
termsearch autostart enable
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Roadmap
|
|
202
|
-
|
|
203
|
-
1. Persistent search stats counter
|
|
204
|
-
2. Engine health tracking and failure classification
|
|
205
|
-
3. Frontend profile viewer panel
|
|
206
|
-
4. Frontend torrent results panel with magnet copy
|
|
207
|
-
5. Agentic user-proxy (`/api/user-proxy`) for custom AI loops
|
|
208
|
-
6. Packaged release on npm registry
|
|
209
|
-
|
|
210
176
|
## License
|
|
211
177
|
|
|
212
178
|
MIT — Copyright (c) 2026 Davide A. Guglielmi
|
package/bin/termsearch.js
CHANGED
|
@@ -32,7 +32,7 @@ function info(msg) { console.log(` ${CYAN}→${RESET} ${msg}`); }
|
|
|
32
32
|
|
|
33
33
|
// ─── Package version ──────────────────────────────────────────────────────
|
|
34
34
|
|
|
35
|
-
let VERSION = '0.3.
|
|
35
|
+
let VERSION = '0.3.8';
|
|
36
36
|
try { VERSION = JSON.parse(readFileSync(PKG_PATH, 'utf8')).version || VERSION; } catch { /* ignore */ }
|
|
37
37
|
|
|
38
38
|
// ─── Data dir + paths ─────────────────────────────────────────────────────
|
package/frontend/dist/app.js
CHANGED
|
@@ -208,6 +208,7 @@ function svg(paths, size = 16, extra = '') {
|
|
|
208
208
|
|
|
209
209
|
const ICONS = {
|
|
210
210
|
search: svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>'),
|
|
211
|
+
filter: svg('<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>'),
|
|
211
212
|
settings: svg('<circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3m-3.7-8.3-2.1 2.1m-8.4 8.4-2.1 2.1m12.5 0-2.1-2.1M5.7 5.7 3.6 3.6"/>'),
|
|
212
213
|
theme: svg('<circle cx="12" cy="12" r="5"/><path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72 1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>'),
|
|
213
214
|
back: svg('<path d="M19 12H5"/><path d="m12 5-7 7 7 7"/>'),
|
|
@@ -267,7 +268,6 @@ const AI_PRESETS = [
|
|
|
267
268
|
{ id: 'chutes', label: 'Chutes.ai TEE', api_base: 'https://llm.chutes.ai/v1', keyRequired: true, defaultModel: 'deepseek-ai/DeepSeek-V3.2-TEE' },
|
|
268
269
|
{ id: 'anthropic',label: 'Anthropic', api_base: 'https://api.anthropic.com/v1', keyRequired: true, defaultModel: 'claude-3-5-haiku-latest' },
|
|
269
270
|
{ id: 'openai', label: 'OpenAI', api_base: 'https://api.openai.com/v1', keyRequired: true, defaultModel: 'gpt-4o-mini' },
|
|
270
|
-
{ id: 'openrouter', label: 'OpenRoute/OpenRouter', api_base: 'https://openrouter.ai/api/v1', keyRequired: true, defaultModel: 'openai/gpt-4o-mini' },
|
|
271
271
|
];
|
|
272
272
|
|
|
273
273
|
const ENGINE_GROUPS = [
|
|
@@ -314,13 +314,19 @@ function LangPicker() {
|
|
|
314
314
|
return wrap;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
function EnginePicker() {
|
|
318
|
-
const
|
|
317
|
+
function EnginePicker(opts = {}) {
|
|
318
|
+
const compact = Boolean(opts.compact);
|
|
319
|
+
const details = el('details', { className: `engine-picker${compact ? ' engine-picker-compact' : ''}` });
|
|
319
320
|
const selectedCount = state.selectedEngines.length;
|
|
320
|
-
const summary =
|
|
321
|
-
el('
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
const summary = compact
|
|
322
|
+
? el('summary', { className: 'engine-picker-summary engine-picker-summary-icon', title: 'Search engines' },
|
|
323
|
+
iconEl('filter', 'engine-filter-icon'),
|
|
324
|
+
selectedCount ? el('span', { className: 'engine-picker-count' }, String(selectedCount)) : null,
|
|
325
|
+
)
|
|
326
|
+
: el('summary', { className: 'engine-picker-summary' },
|
|
327
|
+
el('span', { className: 'engine-picker-title' }, selectedCount ? `Engines (${selectedCount})` : 'Engines (all)'),
|
|
328
|
+
iconEl('chevron', 'engine-chevron'),
|
|
329
|
+
);
|
|
324
330
|
|
|
325
331
|
const body = el('div', { className: 'engine-picker-body' });
|
|
326
332
|
const presetRow = el('div', { className: 'engine-preset-row' });
|
|
@@ -380,6 +386,20 @@ function EnginePicker() {
|
|
|
380
386
|
));
|
|
381
387
|
|
|
382
388
|
details.append(summary, body);
|
|
389
|
+
|
|
390
|
+
// Close on click-outside
|
|
391
|
+
details.addEventListener('toggle', () => {
|
|
392
|
+
if (!details.open) return;
|
|
393
|
+
const onClickOutside = (e) => {
|
|
394
|
+
if (!details.contains(e.target)) {
|
|
395
|
+
details.open = false;
|
|
396
|
+
document.removeEventListener('click', onClickOutside, true);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
// Defer to avoid catching the same click that opened it
|
|
400
|
+
requestAnimationFrame(() => document.addEventListener('click', onClickOutside, true));
|
|
401
|
+
});
|
|
402
|
+
|
|
383
403
|
return details;
|
|
384
404
|
}
|
|
385
405
|
|
|
@@ -1021,7 +1041,8 @@ function renderApp() {
|
|
|
1021
1041
|
el('div', { className: 'header-search' }, SearchForm(state.query, (q, cat) => { state.query = q; doSearch(q, cat); })),
|
|
1022
1042
|
el('div', { className: 'header-nav' },
|
|
1023
1043
|
LangPicker(),
|
|
1024
|
-
|
|
1044
|
+
EnginePicker({ compact: true }),
|
|
1045
|
+
el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
|
|
1025
1046
|
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
1026
1047
|
),
|
|
1027
1048
|
);
|
|
@@ -1047,22 +1068,23 @@ function renderApp() {
|
|
|
1047
1068
|
});
|
|
1048
1069
|
};
|
|
1049
1070
|
buildCatTabs(categoryBar);
|
|
1050
|
-
categoryBar.append(EnginePicker());
|
|
1051
1071
|
|
|
1052
1072
|
const mobileTabs = el('div', { className: 'mobile-bar-tabs' });
|
|
1053
1073
|
buildCatTabs(mobileTabs);
|
|
1054
1074
|
const mobileBar = el('div', { className: 'mobile-bar' },
|
|
1055
1075
|
el('div', { className: 'mobile-bar-search' }, SearchForm(state.query, (q, cat) => { state.query = q; doSearch(q, cat); })),
|
|
1056
1076
|
mobileTabs,
|
|
1057
|
-
el('div', { className: 'mobile-bar-engine' }, EnginePicker()),
|
|
1058
1077
|
el('div', { className: 'mobile-bar-row' },
|
|
1059
1078
|
el('div', {
|
|
1060
1079
|
className: 'mobile-logo',
|
|
1061
1080
|
onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); },
|
|
1062
1081
|
}, 'Term', el('strong', {}, 'Search')),
|
|
1063
1082
|
LangPicker(),
|
|
1064
|
-
el('
|
|
1065
|
-
|
|
1083
|
+
el('div', { className: 'mobile-bar-actions' },
|
|
1084
|
+
EnginePicker({ compact: true }),
|
|
1085
|
+
el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
|
|
1086
|
+
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
1087
|
+
),
|
|
1066
1088
|
),
|
|
1067
1089
|
);
|
|
1068
1090
|
|
|
@@ -1115,6 +1137,33 @@ function renderApp() {
|
|
|
1115
1137
|
}
|
|
1116
1138
|
|
|
1117
1139
|
// ─── Homepage ─────────────────────────────────────────────────────────────
|
|
1140
|
+
function addToBrowser() {
|
|
1141
|
+
// Try legacy Firefox API
|
|
1142
|
+
try {
|
|
1143
|
+
if (window.external?.AddSearchProvider) {
|
|
1144
|
+
window.external.AddSearchProvider(location.origin + '/opensearch.xml');
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
} catch {}
|
|
1148
|
+
// Show inline hint
|
|
1149
|
+
const existing = document.querySelector('.add-browser-hint');
|
|
1150
|
+
if (existing) { existing.remove(); return; }
|
|
1151
|
+
const hint = el('div', { className: 'add-browser-hint' },
|
|
1152
|
+
el('strong', {}, 'Add TermSearch to your browser:'),
|
|
1153
|
+
el('div', { style: 'margin-top:6px' },
|
|
1154
|
+
el('span', { style: 'color:var(--text2)' }, 'Firefox'), ' — click the address bar, look for "Add TermSearch"',
|
|
1155
|
+
),
|
|
1156
|
+
el('div', { style: 'margin-top:3px' },
|
|
1157
|
+
el('span', { style: 'color:var(--text2)' }, 'Chrome'), ' — Settings → Search engine → Manage → Add',
|
|
1158
|
+
),
|
|
1159
|
+
el('div', { style: 'margin-top:3px;font-family:var(--font-mono);font-size:10px;color:var(--text3);word-break:break-all' },
|
|
1160
|
+
`URL: ${location.origin}/#/?q=%s`,
|
|
1161
|
+
),
|
|
1162
|
+
);
|
|
1163
|
+
document.querySelector('.home-actions')?.after(hint);
|
|
1164
|
+
setTimeout(() => hint.remove(), 12000);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1118
1167
|
function renderHome(app) {
|
|
1119
1168
|
const home = el('div', { className: 'home' },
|
|
1120
1169
|
el('div', { className: 'home-logo' }, 'Term', el('strong', {}, 'Search')),
|
|
@@ -1127,6 +1176,8 @@ function renderHome(app) {
|
|
|
1127
1176
|
LangPicker(),
|
|
1128
1177
|
el('button', { className: 'btn', onClick: () => navigate('#/settings') }, iconEl('settings'), ' Settings'),
|
|
1129
1178
|
el('button', { className: 'btn', onClick: toggleTheme }, iconEl('theme'), ' Theme'),
|
|
1179
|
+
el('button', { className: 'btn', onClick: addToBrowser, title: 'Add as browser search engine' }, iconEl('search'), ' Add to browser'),
|
|
1180
|
+
el('a', { className: 'btn', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' }, iconEl('github'), ' GitHub'),
|
|
1130
1181
|
),
|
|
1131
1182
|
);
|
|
1132
1183
|
|
|
@@ -1476,7 +1527,7 @@ async function renderSettings() {
|
|
|
1476
1527
|
el('label', { className: 'form-label', for: 'ai-base' }, 'API Endpoint'),
|
|
1477
1528
|
makeInput('ai-base', ai.api_base, 'http://localhost:11434/v1'),
|
|
1478
1529
|
el('div', { className: 'form-hint' },
|
|
1479
|
-
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI
|
|
1530
|
+
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI',
|
|
1480
1531
|
el('br', {}),
|
|
1481
1532
|
'You can also keep custom OpenAI-compatible endpoints.',
|
|
1482
1533
|
),
|
|
@@ -1620,7 +1671,7 @@ async function renderSettings() {
|
|
|
1620
1671
|
// Server info
|
|
1621
1672
|
el('div', { className: 'settings-section' },
|
|
1622
1673
|
el('h2', {}, 'Server Info'),
|
|
1623
|
-
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.
|
|
1674
|
+
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.8')),
|
|
1624
1675
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Active providers'), el('span', { className: 'info-val' }, (health?.providers || []).join(', ') || 'none')),
|
|
1625
1676
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'AI'), el('span', { className: 'info-val' }, health?.ai_enabled ? `enabled (${health.ai_model})` : 'not configured')),
|
|
1626
1677
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'GitHub'), el('a', { href: 'https://github.com/DioNanos/termsearch', target: '_blank', className: 'info-val', style: 'color:var(--link)' }, 'DioNanos/termsearch')),
|
package/frontend/dist/style.css
CHANGED
|
@@ -161,7 +161,6 @@ a:hover { color: var(--link-h); }
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
.engine-picker {
|
|
164
|
-
margin-left: auto;
|
|
165
164
|
position: relative;
|
|
166
165
|
}
|
|
167
166
|
.engine-picker summary {
|
|
@@ -182,6 +181,44 @@ a:hover { color: var(--link-h); }
|
|
|
182
181
|
font-size: 11px;
|
|
183
182
|
cursor: pointer;
|
|
184
183
|
}
|
|
184
|
+
.engine-picker-summary-icon {
|
|
185
|
+
width: 32px;
|
|
186
|
+
height: 32px;
|
|
187
|
+
padding: 0;
|
|
188
|
+
border-radius: var(--radius-sm);
|
|
189
|
+
justify-content: center;
|
|
190
|
+
background: transparent;
|
|
191
|
+
color: var(--text3);
|
|
192
|
+
border: 1px solid transparent;
|
|
193
|
+
position: relative;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
transition: color 0.15s;
|
|
196
|
+
}
|
|
197
|
+
.engine-picker-summary-icon:hover {
|
|
198
|
+
color: var(--text);
|
|
199
|
+
}
|
|
200
|
+
.engine-filter-icon {
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
}
|
|
204
|
+
.engine-picker-count {
|
|
205
|
+
position: absolute;
|
|
206
|
+
top: -5px;
|
|
207
|
+
right: -5px;
|
|
208
|
+
min-width: 14px;
|
|
209
|
+
height: 14px;
|
|
210
|
+
border-radius: 999px;
|
|
211
|
+
padding: 0 3px;
|
|
212
|
+
display: inline-flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
background: rgba(109,40,217,0.95);
|
|
216
|
+
color: #fff;
|
|
217
|
+
border: 1px solid rgba(167,139,250,0.55);
|
|
218
|
+
font-size: 9px;
|
|
219
|
+
font-weight: 700;
|
|
220
|
+
line-height: 1;
|
|
221
|
+
}
|
|
185
222
|
.engine-picker-title {
|
|
186
223
|
white-space: nowrap;
|
|
187
224
|
}
|
|
@@ -898,6 +935,20 @@ a:hover { color: var(--link-h); }
|
|
|
898
935
|
/* ─── No results ──────────────────────────────────────────────────────────── */
|
|
899
936
|
.no-results { text-align: center; padding: 40px 20px; color: var(--text3); font-size: 14px; }
|
|
900
937
|
|
|
938
|
+
/* ─── Add to browser hint ─────────────────────────────────────────────────── */
|
|
939
|
+
.add-browser-hint {
|
|
940
|
+
max-width: 380px;
|
|
941
|
+
margin: 16px auto 0;
|
|
942
|
+
padding: 12px 16px;
|
|
943
|
+
border-radius: var(--radius);
|
|
944
|
+
background: rgba(30,27,75,0.35);
|
|
945
|
+
border: 1px solid rgba(109,40,217,0.25);
|
|
946
|
+
color: var(--text3);
|
|
947
|
+
font-size: 11px;
|
|
948
|
+
line-height: 1.6;
|
|
949
|
+
animation: fadeIn 0.2s ease;
|
|
950
|
+
}
|
|
951
|
+
|
|
901
952
|
/* ─── Footer ──────────────────────────────────────────────────────────────── */
|
|
902
953
|
.footer {
|
|
903
954
|
border-top: 1px solid var(--border2);
|
|
@@ -962,7 +1013,6 @@ a:hover { color: var(--link-h); }
|
|
|
962
1013
|
scrollbar-width: none;
|
|
963
1014
|
}
|
|
964
1015
|
.mobile-bar-tabs::-webkit-scrollbar { display: none; }
|
|
965
|
-
.mobile-bar-engine { padding: 0 12px 4px; }
|
|
966
1016
|
.mobile-bar-row {
|
|
967
1017
|
display: flex;
|
|
968
1018
|
align-items: center;
|
|
@@ -970,6 +1020,12 @@ a:hover { color: var(--link-h); }
|
|
|
970
1020
|
padding: 4px 12px 2px;
|
|
971
1021
|
}
|
|
972
1022
|
.mobile-bar-row .lang-wrap { margin-left: auto; }
|
|
1023
|
+
.mobile-bar-actions {
|
|
1024
|
+
display: flex;
|
|
1025
|
+
align-items: center;
|
|
1026
|
+
gap: 4px;
|
|
1027
|
+
flex-shrink: 0;
|
|
1028
|
+
}
|
|
973
1029
|
|
|
974
1030
|
/* ─── Responsive ──────────────────────────────────────────────────────────── */
|
|
975
1031
|
@media (max-width: 640px) {
|
|
@@ -978,9 +1034,18 @@ a:hover { color: var(--link-h); }
|
|
|
978
1034
|
.main { padding-bottom: calc(20px + var(--mobile-bar-height, 0px) + env(safe-area-inset-bottom, 0px)); }
|
|
979
1035
|
|
|
980
1036
|
.cat-tab { font-size: 10px; padding: 4px 8px; }
|
|
981
|
-
.mobile-bar-
|
|
982
|
-
.mobile-bar-
|
|
983
|
-
.engine-picker-body {
|
|
1037
|
+
.mobile-bar-row .engine-picker-summary-icon { width: 30px; height: 30px; }
|
|
1038
|
+
.mobile-bar-actions .engine-picker-summary-icon { width: 30px; height: 30px; }
|
|
1039
|
+
.engine-picker-body {
|
|
1040
|
+
position: fixed;
|
|
1041
|
+
left: 10px;
|
|
1042
|
+
right: 10px;
|
|
1043
|
+
width: auto;
|
|
1044
|
+
max-height: min(62vh, 520px);
|
|
1045
|
+
bottom: calc(var(--mobile-bar-height, 0px) + env(safe-area-inset-bottom, 0px) + 10px);
|
|
1046
|
+
top: auto;
|
|
1047
|
+
z-index: 260;
|
|
1048
|
+
}
|
|
984
1049
|
.logo-text { font-size: 15px; }
|
|
985
1050
|
.home-logo { font-size: 40px; }
|
|
986
1051
|
.home-tagline { letter-spacing: 0.08em; margin-bottom: 20px; }
|
package/package.json
CHANGED
package/src/api/routes.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// All API route handlers
|
|
2
2
|
|
|
3
3
|
import express from 'express';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
4
7
|
import { search, searchStream, getEnabledProviders, getDocCache, ALLOWED_ENGINES } from '../search/engine.js';
|
|
5
8
|
import { batchFetch, fetchReadableDocument } from '../fetch/document.js';
|
|
6
9
|
import { generateSummary, testConnection } from '../ai/orchestrator.js';
|
|
@@ -11,7 +14,17 @@ import { detectProfileTarget, scanProfile, PROFILER_PLATFORMS } from '../profile
|
|
|
11
14
|
import { fetchBlueskyPosts, fetchBlueskyActors, fetchGdeltArticles } from '../social/search.js';
|
|
12
15
|
import { scrapeTPB, scrape1337x, extractMagnetFromUrl } from '../torrent/scrapers.js';
|
|
13
16
|
|
|
14
|
-
const
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const APP_VERSION = (() => {
|
|
20
|
+
try {
|
|
21
|
+
const pkgPath = path.join(__dirname, '../../package.json');
|
|
22
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
23
|
+
return String(pkg.version || '0.0.0');
|
|
24
|
+
} catch {
|
|
25
|
+
return '0.0.0';
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
15
28
|
const ALLOWED_CATEGORIES = new Set(['web', 'images', 'news']);
|
|
16
29
|
const ALLOWED_LANGS = new Set(['auto', 'it-IT', 'en-US', 'es-ES', 'fr-FR', 'de-DE', 'pt-PT', 'ru-RU', 'zh-CN', 'ja-JP']);
|
|
17
30
|
|
package/src/server.js
CHANGED
|
@@ -36,14 +36,27 @@ app.use(router);
|
|
|
36
36
|
|
|
37
37
|
// Serve frontend static files
|
|
38
38
|
app.use(express.static(FRONTEND_DIST, {
|
|
39
|
-
maxAge:
|
|
39
|
+
maxAge: 0,
|
|
40
40
|
etag: true,
|
|
41
41
|
index: 'index.html',
|
|
42
|
+
setHeaders: (res, filePath) => {
|
|
43
|
+
const base = path.basename(filePath);
|
|
44
|
+
if (base === 'index.html' || base === 'app.js' || base === 'style.css') {
|
|
45
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
46
|
+
res.setHeader('Pragma', 'no-cache');
|
|
47
|
+
res.setHeader('Expires', '0');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
51
|
+
},
|
|
42
52
|
}));
|
|
43
53
|
|
|
44
54
|
// SPA fallback — serve index.html for any non-API route
|
|
45
55
|
app.get('*', (req, res) => {
|
|
46
56
|
applySecurityHeaders(res);
|
|
57
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
58
|
+
res.setHeader('Pragma', 'no-cache');
|
|
59
|
+
res.setHeader('Expires', '0');
|
|
47
60
|
res.sendFile(path.join(FRONTEND_DIST, 'index.html'));
|
|
48
61
|
});
|
|
49
62
|
|