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 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 ─────────────────────────────────────────────────────
@@ -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 details = el('details', { className: 'engine-picker' });
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 = el('summary', { className: 'engine-picker-summary' },
321
- el('span', { className: 'engine-picker-title' }, selectedCount ? `Engines (${selectedCount})` : 'Engines (all)'),
322
- iconEl('chevron', 'engine-chevron'),
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
- el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
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('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
1065
- 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
+ ),
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 · OpenRoute/OpenRouter',
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.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')),
@@ -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-engine .engine-picker { width: 100%; margin-left: 0; }
982
- .mobile-bar-engine .engine-picker-summary { width: 100%; justify-content: space-between; }
983
- .engine-picker-body { width: calc(100vw - 24px); right: -6px; bottom: calc(100% + 8px); top: auto; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termsearch",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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
 
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