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 CHANGED
@@ -1,187 +1,167 @@
1
- # TermSearch - Personal Search Engine
1
+ # TermSearch
2
2
 
3
- [![Status](https://img.shields.io/badge/Status-0.3.3-blue.svg)](#project-status)
3
+ [![Version](https://img.shields.io/badge/version-0.3.8-blue.svg)](#)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org)
6
- [![Target](https://img.shields.io/badge/Target-Termux%20%2F%20Linux%20%2F%20macOS-green.svg)](https://termux.dev)
6
+ [![Platform](https://img.shields.io/badge/Termux%20%7C%20Linux%20%7C%20macOS-green.svg)](#)
7
7
  [![npm](https://img.shields.io/badge/npm-termsearch-red.svg)](https://www.npmjs.com/package/termsearch)
8
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.
9
+ **Personal search engine.** One command install, zero config, privacy-first.
11
10
 
12
- Core capabilities:
11
+ No Docker, no Python, no API keys required. AI is optional. Everything runs from a single `npm install`.
13
12
 
14
- - Zero-config search via DuckDuckGo and Wikipedia — works immediately after install
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
- ## Project Status
15
+ ```bash
16
+ npm install -g termsearch
17
+ termsearch
18
+ ```
26
19
 
27
- - Current line: `0.3.3`
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
- ## Quickstart
22
+ ## What You Get
34
23
 
35
- 1. Install globally
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
- ```bash
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
- 2. Start
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
- ```bash
44
- termsearch
45
- ```
30
+ **Torrent Search** — The Pirate Bay + 1337x with magnet links, seeders, file sizes.
46
31
 
47
- 3. Open browser at `http://localhost:3000`
32
+ **Social & News** Bluesky posts + GDELT articles inline.
48
33
 
49
- That's it. No configuration required for basic search.
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
- ```bash
54
- termsearch # start + open browser (if not running: status)
55
- termsearch start # start server in background
56
- termsearch start --fg # start in foreground (debug/logs live)
57
- termsearch stop # stop background server
58
- termsearch restart # restart
59
- termsearch status # show PID, URL, uptime
60
- termsearch doctor # check Node version, data dir, HTTP health
61
- termsearch logs # last 60 lines of server log
62
- termsearch logs -n 100 # last N lines
63
- termsearch open # open browser
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
- ```bash
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
- ## Progressive Enhancement
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
- | Level | Requirements | Features |
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
- ## AI Configuration
82
+ **Self-hosted**: SearXNG (proxy to 40+ engines)
87
83
 
88
- Configure at **Settings AI** in the browser. Supported endpoints:
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
- | Provider | API Base | Model | Key |
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
- All providers use the OpenAI-compatible `/chat/completions` format. Leave API key empty for local models.
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 user settings (saved via browser Settings page)
108
- cache/ search + document cache (L1 RAM + L2 disk)
109
- termsearch.pid daemon PID
110
- termsearch.log server 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
- config/ config manager load/save/defaults/env overrides
106
+ server.js Express app (~70 lines)
107
+ config/ manager + defaults + env overrides
114
108
  search/
115
- providers/ DuckDuckGo, Wikipedia, Brave, Mojeek, SearXNG, GitHub API, Yandex, Ahmia, Marginalia
116
- engine.js fan-out, merge, rank, cache
117
- ranking.js source diversity ranking
118
- cache.js tiered cache (L1 Map + L2 disk JSON)
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 2-phase agentic summary flow
124
- summary.js prompt builder + response parser
125
- query.js query refinement
126
- providers/
127
- openai-compat.js universal OpenAI-compatible client
128
- profiler/
129
- scanner.js social profile scanner (10 platforms)
130
- social/
131
- scrapers.js Twitter/Nitter, Instagram, YouTube, Facebook, LinkedIn, TikTok, Telegram
132
- search.js Bluesky posts/actors + GDELT news
133
- torrent/
134
- scrapers.js TPB + 1337x scrapers + magnet extraction
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 service status + enabled providers
149
- GET /api/openapi.json machine-readable API description
150
- GET /api/search?q=... web search
151
- GET /api/search-stream?q=... progressive SSE search
152
- POST /api/fetch fetch readable content from URL
153
- POST /api/ai-query AI query refinement
154
- POST /api/ai-summary AI summary with SSE streaming
155
- GET /api/profiler?q=... social profile scan (URL or @handle)
156
- GET /api/social-search?q=... Bluesky + GDELT news
157
- POST /api/torrent-search torrent search (TPB + 1337x)
158
- POST /api/magnet extract magnet from torrent page URL
159
- POST /api/scan crawl site for query-matched pages
160
- GET /api/config current config (keys masked)
161
- POST /api/config update and persist config
162
- POST /api/config/test-ai test AI connection
163
- GET /api/config/test-provider/:name test search provider
164
- GET /api/autostart autostart status
165
- POST /api/autostart enable/disable autostart
166
- GET /api/stats usage stats
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
- ## Environment Variables (optional, override Settings)
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=https://api.z.ai/api/coding/paas/v4
158
+ TERMSEARCH_AI_API_BASE=http://localhost:11434/v1
176
159
  TERMSEARCH_AI_API_KEY=
177
- TERMSEARCH_AI_MODEL=glm-4.7
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.1';
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 ─────────────────────────────────────────────────────
@@ -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', onClick: () => navigate('#/settings') }, iconEl('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
- EnginePicker({ compact: true }),
1071
- el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
1072
- el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
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 · OpenRoute/OpenRouter',
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.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')),
@@ -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-color: var(--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
- border-color: #3a3a3a;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termsearch",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Personal search engine for Termux/Linux/macOS — zero-config, privacy-first, AI-optional",
5
5
  "type": "module",
6
6
  "bin": {
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 APP_VERSION = '0.3.3';
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: '1h',
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>