termsearch 0.3.7 → 0.3.9
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 +56 -7
- package/frontend/dist/style.css +90 -3
- package/package.json +1 -1
- package/src/api/routes.js +31 -1
- package/src/server.js +14 -1
- package/frontend/dist/opensearch.xml +0 -8
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
|
@@ -268,7 +268,6 @@ const AI_PRESETS = [
|
|
|
268
268
|
{ id: 'chutes', label: 'Chutes.ai TEE', api_base: 'https://llm.chutes.ai/v1', keyRequired: true, defaultModel: 'deepseek-ai/DeepSeek-V3.2-TEE' },
|
|
269
269
|
{ id: 'anthropic',label: 'Anthropic', api_base: 'https://api.anthropic.com/v1', keyRequired: true, defaultModel: 'claude-3-5-haiku-latest' },
|
|
270
270
|
{ id: 'openai', label: 'OpenAI', api_base: 'https://api.openai.com/v1', keyRequired: true, defaultModel: 'gpt-4o-mini' },
|
|
271
|
-
{ id: 'openrouter', label: 'OpenRoute/OpenRouter', api_base: 'https://openrouter.ai/api/v1', keyRequired: true, defaultModel: 'openai/gpt-4o-mini' },
|
|
272
271
|
];
|
|
273
272
|
|
|
274
273
|
const ENGINE_GROUPS = [
|
|
@@ -387,6 +386,20 @@ function EnginePicker(opts = {}) {
|
|
|
387
386
|
));
|
|
388
387
|
|
|
389
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
|
+
|
|
390
403
|
return details;
|
|
391
404
|
}
|
|
392
405
|
|
|
@@ -1029,7 +1042,7 @@ function renderApp() {
|
|
|
1029
1042
|
el('div', { className: 'header-nav' },
|
|
1030
1043
|
LangPicker(),
|
|
1031
1044
|
EnginePicker({ compact: true }),
|
|
1032
|
-
el('button', { className: 'btn-icon', title: 'Settings',
|
|
1045
|
+
el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
|
|
1033
1046
|
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
1034
1047
|
),
|
|
1035
1048
|
);
|
|
@@ -1067,9 +1080,11 @@ function renderApp() {
|
|
|
1067
1080
|
onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); },
|
|
1068
1081
|
}, 'Term', el('strong', {}, 'Search')),
|
|
1069
1082
|
LangPicker(),
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
+
),
|
|
1073
1088
|
),
|
|
1074
1089
|
);
|
|
1075
1090
|
|
|
@@ -1122,6 +1137,38 @@ function renderApp() {
|
|
|
1122
1137
|
}
|
|
1123
1138
|
|
|
1124
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 searchUrl = `${location.origin}/#/?q=%s`;
|
|
1152
|
+
const urlBox = el('div', { className: 'add-browser-url' });
|
|
1153
|
+
const urlText = el('code', {}, searchUrl);
|
|
1154
|
+
const copyBtn = el('button', { className: 'add-browser-copy', type: 'button', onClick: () => {
|
|
1155
|
+
navigator.clipboard.writeText(searchUrl).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000); }).catch(() => {});
|
|
1156
|
+
} }, 'Copy');
|
|
1157
|
+
urlBox.append(urlText, copyBtn);
|
|
1158
|
+
const hint = el('div', { className: 'add-browser-hint' },
|
|
1159
|
+
el('div', { className: 'add-browser-title' }, 'Add TermSearch to your browser'),
|
|
1160
|
+
el('div', { className: 'add-browser-steps' },
|
|
1161
|
+
el('div', {}, el('span', { className: 'add-browser-badge' }, 'Firefox'), ' Address bar → click TermSearch icon → "Add"'),
|
|
1162
|
+
el('div', {}, el('span', { className: 'add-browser-badge' }, 'Chrome'), ' Settings → Search engine → Manage → Add'),
|
|
1163
|
+
el('div', {}, el('span', { className: 'add-browser-badge' }, 'Safari'), ' Not natively supported — use extension or bookmark'),
|
|
1164
|
+
),
|
|
1165
|
+
el('div', { className: 'add-browser-label' }, 'Search URL (paste in browser settings):'),
|
|
1166
|
+
urlBox,
|
|
1167
|
+
);
|
|
1168
|
+
document.querySelector('.home-actions')?.after(hint);
|
|
1169
|
+
setTimeout(() => hint.remove(), 20000);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1125
1172
|
function renderHome(app) {
|
|
1126
1173
|
const home = el('div', { className: 'home' },
|
|
1127
1174
|
el('div', { className: 'home-logo' }, 'Term', el('strong', {}, 'Search')),
|
|
@@ -1134,6 +1181,8 @@ function renderHome(app) {
|
|
|
1134
1181
|
LangPicker(),
|
|
1135
1182
|
el('button', { className: 'btn', onClick: () => navigate('#/settings') }, iconEl('settings'), ' Settings'),
|
|
1136
1183
|
el('button', { className: 'btn', onClick: toggleTheme }, iconEl('theme'), ' Theme'),
|
|
1184
|
+
el('button', { className: 'btn', onClick: addToBrowser, title: 'Add as browser search engine' }, iconEl('search'), ' Add to browser'),
|
|
1185
|
+
el('a', { className: 'btn', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' }, iconEl('github'), ' GitHub'),
|
|
1137
1186
|
),
|
|
1138
1187
|
);
|
|
1139
1188
|
|
|
@@ -1483,7 +1532,7 @@ async function renderSettings() {
|
|
|
1483
1532
|
el('label', { className: 'form-label', for: 'ai-base' }, 'API Endpoint'),
|
|
1484
1533
|
makeInput('ai-base', ai.api_base, 'http://localhost:11434/v1'),
|
|
1485
1534
|
el('div', { className: 'form-hint' },
|
|
1486
|
-
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI
|
|
1535
|
+
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI',
|
|
1487
1536
|
el('br', {}),
|
|
1488
1537
|
'You can also keep custom OpenAI-compatible endpoints.',
|
|
1489
1538
|
),
|
|
@@ -1627,7 +1676,7 @@ async function renderSettings() {
|
|
|
1627
1676
|
// Server info
|
|
1628
1677
|
el('div', { className: 'settings-section' },
|
|
1629
1678
|
el('h2', {}, 'Server Info'),
|
|
1630
|
-
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.
|
|
1679
|
+
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.8')),
|
|
1631
1680
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Active providers'), el('span', { className: 'info-val' }, (health?.providers || []).join(', ') || 'none')),
|
|
1632
1681
|
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')),
|
|
1633
1682
|
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
|
@@ -189,12 +189,13 @@ a:hover { color: var(--link-h); }
|
|
|
189
189
|
justify-content: center;
|
|
190
190
|
background: transparent;
|
|
191
191
|
color: var(--text3);
|
|
192
|
-
border
|
|
192
|
+
border: 1px solid transparent;
|
|
193
193
|
position: relative;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
transition: color 0.15s;
|
|
194
196
|
}
|
|
195
197
|
.engine-picker-summary-icon:hover {
|
|
196
|
-
|
|
197
|
-
color: var(--text2);
|
|
198
|
+
color: var(--text);
|
|
198
199
|
}
|
|
199
200
|
.engine-filter-icon {
|
|
200
201
|
display: inline-flex;
|
|
@@ -934,6 +935,85 @@ a:hover { color: var(--link-h); }
|
|
|
934
935
|
/* ─── No results ──────────────────────────────────────────────────────────── */
|
|
935
936
|
.no-results { text-align: center; padding: 40px 20px; color: var(--text3); font-size: 14px; }
|
|
936
937
|
|
|
938
|
+
/* ─── Add to browser hint ─────────────────────────────────────────────────── */
|
|
939
|
+
.add-browser-hint {
|
|
940
|
+
max-width: 420px;
|
|
941
|
+
margin: 20px auto 0;
|
|
942
|
+
padding: 16px 20px;
|
|
943
|
+
border-radius: var(--radius);
|
|
944
|
+
background: rgba(30,27,75,0.45);
|
|
945
|
+
border: 1px solid rgba(109,40,217,0.35);
|
|
946
|
+
color: var(--text2);
|
|
947
|
+
font-size: 12px;
|
|
948
|
+
line-height: 1.7;
|
|
949
|
+
animation: fadeIn 0.2s ease;
|
|
950
|
+
}
|
|
951
|
+
.add-browser-title {
|
|
952
|
+
font-size: 13px;
|
|
953
|
+
font-weight: 700;
|
|
954
|
+
color: var(--text);
|
|
955
|
+
margin-bottom: 10px;
|
|
956
|
+
}
|
|
957
|
+
.add-browser-steps {
|
|
958
|
+
display: flex;
|
|
959
|
+
flex-direction: column;
|
|
960
|
+
gap: 5px;
|
|
961
|
+
margin-bottom: 12px;
|
|
962
|
+
}
|
|
963
|
+
.add-browser-badge {
|
|
964
|
+
display: inline-block;
|
|
965
|
+
padding: 1px 7px;
|
|
966
|
+
border-radius: 99px;
|
|
967
|
+
font-size: 10px;
|
|
968
|
+
font-weight: 700;
|
|
969
|
+
background: rgba(167,139,250,0.18);
|
|
970
|
+
color: #a78bfa;
|
|
971
|
+
border: 1px solid rgba(167,139,250,0.3);
|
|
972
|
+
letter-spacing: 0.02em;
|
|
973
|
+
}
|
|
974
|
+
.add-browser-label {
|
|
975
|
+
font-size: 10px;
|
|
976
|
+
color: var(--text3);
|
|
977
|
+
text-transform: uppercase;
|
|
978
|
+
letter-spacing: 0.04em;
|
|
979
|
+
margin-bottom: 6px;
|
|
980
|
+
}
|
|
981
|
+
.add-browser-url {
|
|
982
|
+
display: flex;
|
|
983
|
+
align-items: center;
|
|
984
|
+
gap: 8px;
|
|
985
|
+
padding: 8px 12px;
|
|
986
|
+
border-radius: var(--radius-sm);
|
|
987
|
+
background: rgba(0,0,0,0.4);
|
|
988
|
+
border: 1px solid rgba(167,139,250,0.25);
|
|
989
|
+
}
|
|
990
|
+
.add-browser-url code {
|
|
991
|
+
flex: 1;
|
|
992
|
+
min-width: 0;
|
|
993
|
+
font-family: var(--font-mono);
|
|
994
|
+
font-size: 12px;
|
|
995
|
+
font-weight: 600;
|
|
996
|
+
color: #c4b5fd;
|
|
997
|
+
word-break: break-all;
|
|
998
|
+
user-select: all;
|
|
999
|
+
}
|
|
1000
|
+
.add-browser-copy {
|
|
1001
|
+
flex-shrink: 0;
|
|
1002
|
+
padding: 4px 12px;
|
|
1003
|
+
border-radius: var(--radius-sm);
|
|
1004
|
+
border: 1px solid rgba(167,139,250,0.35);
|
|
1005
|
+
background: rgba(109,40,217,0.25);
|
|
1006
|
+
color: #e0d4fd;
|
|
1007
|
+
font-size: 11px;
|
|
1008
|
+
font-weight: 600;
|
|
1009
|
+
cursor: pointer;
|
|
1010
|
+
transition: background 0.15s, border-color 0.15s;
|
|
1011
|
+
}
|
|
1012
|
+
.add-browser-copy:hover {
|
|
1013
|
+
background: rgba(109,40,217,0.45);
|
|
1014
|
+
border-color: rgba(167,139,250,0.55);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
937
1017
|
/* ─── Footer ──────────────────────────────────────────────────────────────── */
|
|
938
1018
|
.footer {
|
|
939
1019
|
border-top: 1px solid var(--border2);
|
|
@@ -1005,6 +1085,12 @@ a:hover { color: var(--link-h); }
|
|
|
1005
1085
|
padding: 4px 12px 2px;
|
|
1006
1086
|
}
|
|
1007
1087
|
.mobile-bar-row .lang-wrap { margin-left: auto; }
|
|
1088
|
+
.mobile-bar-actions {
|
|
1089
|
+
display: flex;
|
|
1090
|
+
align-items: center;
|
|
1091
|
+
gap: 4px;
|
|
1092
|
+
flex-shrink: 0;
|
|
1093
|
+
}
|
|
1008
1094
|
|
|
1009
1095
|
/* ─── Responsive ──────────────────────────────────────────────────────────── */
|
|
1010
1096
|
@media (max-width: 640px) {
|
|
@@ -1014,6 +1100,7 @@ a:hover { color: var(--link-h); }
|
|
|
1014
1100
|
|
|
1015
1101
|
.cat-tab { font-size: 10px; padding: 4px 8px; }
|
|
1016
1102
|
.mobile-bar-row .engine-picker-summary-icon { width: 30px; height: 30px; }
|
|
1103
|
+
.mobile-bar-actions .engine-picker-summary-icon { width: 30px; height: 30px; }
|
|
1017
1104
|
.engine-picker-body {
|
|
1018
1105
|
position: fixed;
|
|
1019
1106
|
left: 10px;
|
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
|
|
|
@@ -218,6 +231,23 @@ export function createRouter(config, rateLimiters) {
|
|
|
218
231
|
});
|
|
219
232
|
});
|
|
220
233
|
|
|
234
|
+
// ─── OpenSearch (dynamic — adapts to host/port) ─────────────────────────
|
|
235
|
+
router.get('/opensearch.xml', (req, res) => {
|
|
236
|
+
const proto = req.protocol;
|
|
237
|
+
const host = req.get('host') || `${req.hostname}:${req.socket.localPort}`;
|
|
238
|
+
const origin = `${proto}://${host}`;
|
|
239
|
+
res.setHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8');
|
|
240
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
241
|
+
res.send(`<?xml version="1.0" encoding="UTF-8"?>
|
|
242
|
+
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
|
243
|
+
<ShortName>TermSearch</ShortName>
|
|
244
|
+
<Description>TermSearch — personal search engine</Description>
|
|
245
|
+
<InputEncoding>UTF-8</InputEncoding>
|
|
246
|
+
<Image height="64" width="64" type="image/svg+xml">${origin}/icon.svg</Image>
|
|
247
|
+
<Url type="text/html" method="get" template="${origin}/#/?q={searchTerms}"/>
|
|
248
|
+
</OpenSearchDescription>`);
|
|
249
|
+
});
|
|
250
|
+
|
|
221
251
|
// ─── OpenAPI ─────────────────────────────────────────────────────────────
|
|
222
252
|
router.get('/api/openapi.json', (_req, res) => {
|
|
223
253
|
applySecurityHeaders(res);
|
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
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
|
3
|
-
<ShortName>TermSearch</ShortName>
|
|
4
|
-
<Description>TermSearch — personal search engine</Description>
|
|
5
|
-
<InputEncoding>UTF-8</InputEncoding>
|
|
6
|
-
<Image height="64" width="64" type="image/svg+xml">/icon.svg</Image>
|
|
7
|
-
<Url type="text/html" method="get" template="http://localhost:3000/#/?q={searchTerms}"/>
|
|
8
|
-
</OpenSearchDescription>
|